Skip to main content

The Rise of Function Components

Free2019-06-23#JS#React Function Component#React Class 组件迁移指南#corresponding Hooks#componentWillUpdate in hooks#Migrate Class to Hooks

With Hooks, function components will have almost the same expressiveness as Class components, including various lifecycles, State, etc.

I. Class Component

Class is undoubtedly the most widely used React component form, for example:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Has the most complete lifecycle support, old and new totaling 13:

// Mounting
constructor()
render()
UNSAFE_componentWillMount()
componentDidMount()

// Updating
static getDerivedStateFromProps()
shouldComponentUpdate()
getSnapshotBeforeUpdate()
UNSAFE_componentWillReceiveProps()
UNSAFE_componentWillUpdate()
componentDidUpdate()

// Unmounting
componentWillUnmount()

// Error Handling
static getDerivedStateFromError()
componentDidCatch()

In addition, there are State, Props, Context, Ref and other features. These supports make Class become the only option with complete component features, although Class also has many problems, but it is irreplaceable

P.S. About the meaning and function of each lifecycle, see React | An Yu Qing Yang

II. Function Component

Another component form is function, input Props, output React Element, for example:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

As the simplest React component form:

The simplest way to define a component is to write a JavaScript function.

Simple to the point of not even having lifecycle, State is needless to say. These limitations determine function components can only be used as very simple View Component, cannot bear heavy responsibilities. For quite a long time, only for "teaching" use:

Classes have some additional features that we will discuss in the next sections. Until then, we will use function components for their conciseness.

Since React 16, gradually enhanced function components:

  • createRef/forwardRef: After React 16.3, function components support Ref

  • [React.memo](/articles/react-16-6 新 api/#articleHeader2): After React 16.6, function components also welcomed "shouldComponentUpdate"

Of course, the most important enhancement is naturally Hooks:

Hooks allow function components to also have State, lifecycle and other Class component features (such as state, lifecycle, context, ref, etc.)

P.S. For detailed information about Hooks, see React Hooks Introduction

III. Function Component with Hooks

Simply speaking, with Hooks, function components will have almost the same expressiveness as Class components, including various lifecycles, State, etc.

If you write a function component and realize you need to add some state to it, previously you had to convert it to a class. Now you can use a Hook inside the existing function component.

For example:

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState Hook lets function components have State, similarly, lifecycle, Context, Ref, component instance properties and other features are all supported through similar Hook methods, specifically as follows:

HookFeatureAnalogy Class
useStateStatethis.state
this.setState(newState)
useEffectLifecyclecomponentDidMount
componentDidUpdate
useContextContextthis.context
useReducerStateRedux Reducer style State management
useCallbackFunction Propsthis.myMethod.bind(this)
useMemoPerformance OptimizationAvoid repeated calculations
useRefRefcreateRef
useImperativeHandleComponent Instance Properties/MethodsforwardRef
useLayoutEffectLifecycleSynchronouscomponentDidMount
SynchronouscomponentDidUpdate
useDebugValueDebuggingHooks state visualization (similar to looking atthis.statefrom React DevTools)

IV. Migrate Class to Hooks

Of course, no need and not too possible to refactor existing Class components to Hooks:

There is no rush to migrate to Hooks. We recommend avoiding any "big rewrites", especially for existing, complex class components.

We intend for Hooks to cover all existing use cases for classes, but we will keep supporting class components for the foreseeable future. At Facebook, we have tens of thousands of components written as classes, and we have absolutely no plans to rewrite them.

Here just says the correspondence relationship between Hooks and Class features, this analogy helps understand Hooks

constructor()

The most critical operation in constructor should be declaring/initializing this.state, completed through State Hook:

class Example extends React.Component {
  constructor() {
    this.state = {
      count: 0
    };
  }
}

Equivalent to:

function Example() {
  // Declare a state variable with initial value 0
  const [count, setCount] = React.useState(0);
}

Its syntax format is:

const [state, setState] = useState(initialState);

Among them const [state, setState] = xxx is a destructuring assignment syntax (specifically see [destructuring (Destructuring Assignment)_ES6 Notes 5](/articles/destructuring(解构赋值)-es6 笔记 5/)), equivalent to:

const stateVariable = useState(initialState); // Returns a pair
const state = stateVariable[0]; // First item in a pair
const setState = stateVariable[1]; // Second item in a pair

Returns state value (state) and corresponding Setter (setState), calling Setter will trigger component update (similar to this.setState in Class)

Initial value initialState only affects first render (taken out through return value state), afterwards state keeps updating

Specially, if need multiple state variables, just call useState several more times:

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}

Of course, state value can also be object or array, but will not merge like this.setState():

Unlike this.setState in a class, updating a state variable always replaces it instead of merging it.

For example:

function ExampleWithManyStates() {
  const [profile, setProfile] = useState({
    age: 42,
    favorite: 'banana',
    todos: [{ text: 'Learn Hooks' }]
  });

  setProfile({
    todos: []
  });
  // Equivalent to in Class
  this.setState({
    age: undefined,
    favorite: undefined,
    todos: []
  });
  // Instead of
  this.setState({
    todos: []
  });
}

render()

Function component itself is a render() function, inject Props, State and other data into view, and register event handling logic:

class Example extends React.Component {
  /* Omit state initialization part */
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

Equivalent to:

function Example() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Different from Class components, function component's State value is obtained through State Hook ( count in above example), not this.state. Correspondingly, this.setState() is also completed through Setter returned by useState()

UNSAFE_componentWillMount()

Triggers before render() during first render, overlaps somewhat with constructor() function, can refer to aforementioned constructor() part

componentDidMount()

Usually has some operations with side effects in componentDidMount, in function components can be replaced with Effect Hook:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  /* Omit render part */
}

Equivalent to:

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
  /* Omit return part */
}

Effect Hook triggers after component finishes every render, therefore equivalent to Class component's componentDidMount, componentDidUpdate and componentWillUnmount

Syntax format is:

useEffect(didUpdate);

Indicates component needs to do something after every (including first) render:

The function passed to useEffect will run after the render is committed to the screen.

If need to distinguish mounting and updating (componentDidMount and componentDidUpdate), can complete through declaring dependencies, specifically see Tip: Optimizing Performance by Skipping Effects

And for side effects that only need to execute/cleanup once, just declare it doesn't depend on any component state (useEffect(didUpdate, [])), at this time equivalent to componentDidMount plus componentWillUnmount

However, due to Fiber scheduling mechanism, Effect Hook is not triggered synchronously. Therefore if need to read DOM state, use synchronous LayoutEffect Hook

P.S. So, strictly speaking, LayoutEffect Hook is the Hooks API equivalent to componentDidMount, componentDidUpdate and other lifecycles. But for performance/user experience considerations, recommend prioritizing use of Effect Hook

Specially, there are some side effects that need corresponding cleanup work, such as canceling subscription to external data sources (avoid memory leaks):

class FriendStatus extends React.Component {
  /* Omit state initialization part */
  componentDidMount() {
    // Subscribe
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    // Unsubscribe
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  /* Omit render part, and handleStatusChange */
}

Equivalent to:

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    // Subscribe
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Unsubscribe
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

As above example, Effect Hook provides Disposable mechanism to support cleanup operations, but Hooks' operation mechanism determines cleanup work will be triggered after every render:

Effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.

If repeated subscription has performance impact, can also solve through declaring dependencies (in future might automatically find dependencies at compile time)

Additionally, similar to multiple useState(), can also separate different Effects through multiple useEffect():

Just like you can use the State Hook more than once, you can also use several effects. This lets us separate unrelated logic into different effects.

For example:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  // DOM Operation Effect
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  // Data Subscription Effect
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

static getDerivedStateFromProps()

getDerivedStateFromProps is used to replace componentWillReceiveProps, coping with scenarios where state needs to associate with props changes

(Extracted from [II. How to Understand getDerivedStateFromProps](/articles/从 componentwillreceiveprops 说起/#articleHeader2))

In function components, for scenarios where props changes cause state changes, can directly complete through State Hook, for example recording scroll direction:

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.currentRow !== state.lastRow) {
      return {
        isScrollingDown: props.currentRow > state.lastRow,
        lastRow: props.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

Equivalent to:

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

shouldComponentUpdate()

In function components, use [React.memo](/articles/react-16-6 新 api/#articleHeader2) to replace

getSnapshotBeforeUpdate()

Temporarily (2019/06/23) no replaceable Hooks API, but will add soon

UNSAFE_componentWillReceiveProps()

As aforementioned, componentWillReceiveProps and getDerivedStateFromProps both replaced with State Hook, see constructor() part

UNSAFE_componentWillUpdate()

componentWillUpdate generally can be replaced with componentDidUpdate, if need to read DOM state, replace with getSnapshotBeforeUpdate:

Typically, this method can be replaced by componentDidUpdate(). If you were reading from the DOM in this method (e.g. to save a scroll position), you can move that logic to getSnapshotBeforeUpdate().

Therefore, componentWillUpdate generally can be replaced with Effect Hook or LayoutEffect Hook, see componentDidMount() part

componentDidUpdate()

As aforementioned, componentDidUpdate can be replaced with Effect Hook, see componentDidMount() part

componentWillUnmount()

As aforementioned, componentWillUnmount can be replaced with Effect Hook, see componentDidMount() part

static getDerivedStateFromError()

Temporarily (2019/06/23) no replaceable Hooks API, but will add soon

componentDidCatch()

Temporarily (2019/06/23) no replaceable Hooks API, but will add soon

Context

Function components can also access Context, and reading method is simpler:

// Declare
const {Provider, Consumer} = React.createContext(defaultValue);
// Write
<Provider value={/* some value */}>
// Read
<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

Equivalent to:

// Declare
const MyContext = React.createContext(defaultValue);
const Provider = MyContext.Provider;
// Write
<Provider value={/* some value */}>
// Read
const value = useContext(MyContext);

Ref

Ref also provides similar support:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

Equivalent to:

function MyComponent() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return <input type="text" ref={inputRef} />;
}

That is:

const refContainer = useRef(initialValue);
// Equivalent to
const refContainer = React.createRef();

Instance Variables

Interestingly, Ref can also be used to retain component instance state (equivalent to this.xxx), for example:

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });
}

Further, can implement componentDidUpdate through this.mounted:

function FunctionComponent(props) {
  // Flag for strict Update lifecycles
  const mounted = useRef();

  useEffect(() => {
    if (mounted.current) {
      // componentDidUpdate
    }
  });
  useEffect(() => {
    mounted.current = true;
  }, []);
  // ...
}

Instance Method

We can reference Class component instance through Ref, thereby access its instance methods:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  // Instance method
  focus() {
    this.inputRef.current.focus();
  }
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

class App extends React.Component {
  componentDidMount() {
    this.myComponent.focus();
  }
  render() {
    return <MyComponent ref={ins => this.myComponent = ins} />;
  }
}

Equivalent to:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);

class App extends React.Component {
  componentDidMount() {
    this.myComponent.focus();
  }
  render() {
    return <FancyInput ref={ins => this.myComponent = ins} />;
  }
}

V. Online Demo

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment