The difference between functional components and class components

First, it’s important to be clear that while the optimization strategies for functional and class components are not quite the same, the performance differences in modern browsers are minimal when optimized properly. Performance depends primarily on the code, not on the difference between choosing a function component and a class component per se, so you don’t have to think about which way to write code from a performance perspective.

Here are some differences between functional components and class components,

1) State synchronization problem,The function component captures the values used in the current rendering. But this is often overlooked.

Take a look at this example, state synchronization, and follow these steps:

1. Enter 111 2. Click the button 3Copy the code

Through this operation, we can see that the pop-up value in the functional component is the value at the moment the button is clicked, and the class component is the latest value.

Now let’s analyze the reasons for this discrepancy.

First of all, we know that a functional component or a class component will be re-rendered whenever its props or state changes, and for a child component that has not been optimized for performance, the child component will be re-rendered whenever the parent component is re-rendered. And in React, props are immutable, whereas this is always changing. So the method in the class component can get the latest instance, which is this, while the function component captures the render value because of the closure.

That we how to make the class component rendering when the value of the component or function for the latest value, look at the example below, the type of component using closures, using useRef function components, we defined function for class component in the render function, so that we formed a closure, can like function components capture the corresponding values in rendering; For function components we use useRef to get the latest value because useRef returns a variable value.

Looking at the issue of state synchronization, the function component and the class component timer, we find that the two components achieve the same effect. But if we take a closer look at the function component, we created a timer in useEffect, but every time the count value changes, the component re-renders and clears the timer and creates a new timer, which is obviously not what we want. But if you remove the count dependency, you’ll find that the count value stays at 1 forever. The useEffect timer captures the value of count at the time of rendering, so the count value always changes from 0 to 1.

So that’s the first difference, the synchronization of the states.

2) useEffect and class component lifecycle

Let’s start with a few lifecycle functions in class components:

ShouldComponentUpdate () {} shouldComponentUpdate() {} componentDidUpdate() {} ComponentWillUnmount () {}. Usually in componentDidMount we do some DOM-dependent initialization, do network requests, event bindings, subscriptions, etc.; Perform some DOM operations and network requests at componentDidUpdate; At componentWillUnmount there is some unbundling and unsubscribing.

Compared to function components, these operations can be implemented through the useEffect hook, but useEffect is more cumbersome to manage than the life cycle of class components, especially in the case of complex business logic.

First, when we implement useEffect, we usually pass in a second parameter dependency array to avoid executing its callback every time render executes it. Thus, the useEffect callback is executed only when the dependent array changes. But when the business logic is complex, it can lead to too many problems. So you might have code like this in your project:

useEffect(() => { // ... }, [name, searchState, address, status, personA, personB, progress, page, size]); Copy the codeCopy the code

If you have multiple parts of this code in a component, it’s complicated to maintain these dependencies, let alone the business logic. This leads to several thoughts:

  • 1. Should you use multiple states or a single state?
  • 2. How to reduce dependencies?

The first problem is that because states defined through useState often become dependency injection dependency arrays, separating all the states can lead to too many dependencies. However, defining a single state, like this.state in a class component, would result in all the business logic in a useEffect, which is not conducive to the maintenance of the code and the separation of the business logic through useEffect splitting. Therefore, combined with the actual situation, states of the same class can be defined together as follows:

Const [pagination, setPagination] = useState({current: 1, pageSize: Const [position, setPosition] = useState({left: 0, top: 0}Copy the code

There are several ways to reduce dependencies:

  • Break up the hooks into smaller units, with each Hook dependent on its own dependency array.
  • Multiple dependent values are aggregated into one by merging the associated states.
  • Use the setState callback to get the latest state to reduce external dependencies, as shown in the timer example.
  • Use the useReducer to decouple updates from actions. The drawback of the previous approach is that if you rely on two states then you have to add a dependency. Look at the following code
useEffect(() => { const id = setInterval(() => { setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [step]); Copy the codeCopy the code

The solution is to use useReducer to decouple the update and action. See this example, useReducer decouples the update and action. We can see from the code that the update operation in useEffect only relies on the function dispatch. And this function is never going to change, so there’s no dependency here.

Maintenance of dependent arrays is covered in the performance tuning section below. As can be seen from the above, when components and business logic are very complex, reactive useEffect is very troublesome to manage. A class component takes the stress out of management.

3) Performance optimization

ShouldComponentUpdate this lifecycle is used to optimize components by checking whether the previous props and the current props are changed, or by PureComponent.

React.memo(), then click the increment count button and look at the console. Only “NotUseMemoComponent” is printed. This means that if the value passed by the parent component to the child component does not change, the child component that uses memo will not be re-rendered by the parent component, while the child component that does not use Memo will be rendered by the parent component as well.

But when a parent component passes a value of its own defined reference type to a child component, even if the value does not change. But since each rendering will generate a new variable, the result in reference changed, so the child components will still rendering, specific look at this example, the transfer function object or array, the print shows that each parent component rendering will generate new sayHi function, this makes the child component rendering and heavy because useEffect depend on this function, UseEffect is also re-executed. This results in a lot of useless rendering of the child components.

In this case, we usually consider using useCallback,useMemo for optimization. Look at the following example, useCallback,useMemo. Now we find that even if we keep clicking on the button, it does not re-trigger the rendering of the child component, and useEffect does not execute. This is because useCallback, useMemo reads the cache without regenerating functions or objects, even if the dependency array remains unchanged.

Note that the state defined with useState and the methods used to change the state if they become dependencies do not cause the callback function to be re-executed due to rerendering, so there is no need to wrap useCallback or useMemo.

4) Code reuse

Assume that there are four components A, B, C, and D. B and D are components with different UI but similar business logic. After obtaining data from the server, the list data is displayed in A loop, and the structure is roughly as follows

<A> <B /> </A> <C> <D /> </C> Copy the codeCopy the code

Function components: Custom hooks

Originally, we need to carry out state definition and data acquisition in B and D components. B and D components are as follows:

The component B/D:  import React, {useState, useEffect} from 'react' import axios from 'axios' function B() { const [lists, setLists] = useState([]) useEffect(() => { const getLists = async () => { const data = await axios.get('xxx/xxxx') // Data request address setLists(data)} getLists()}, []) return(// render <> {lists. Map (item) => ()} </>)} export default B Copy codeCopy the code

Now you can customize a hook to write the same code only once, as follows:

UseAxios. Js:  import {useState, useEffect} from 'react' import axios from 'axios' function useAxios(name) { const [lists, setLists] = useState([]) useEffect(() => { const getLists = async () => { const data = await axios.get(name) SetLists (data)} getLists()}, [name]) return lists} export default useAxios component B/D:  import React from 'react' import useAxios from '.. /customHooks/useAxios' function B() {const lists = useAxios(' XXX/XXXX ')// Render <> {lists. Map (item) => ()} </>)} export default B Copies the codeCopy the code

The following comparison is implemented with a class component:

Class components: HOC(high-level components) and Render Props

The component B/D: import React, {Component} from 'react' import axios from 'axios' class B extends Component { constructor () { super() this.state = { Lists: []}} componentWillMount () {const data = await axios.get(' XXX/XXXX ') Data})} render () {// render return (<> {this.state.lists. Map (item) => ()} </>)}} export default B Copy codeCopy the code

Using HOC (high order component written as follows) :

Wrapwithajax.js (this is a HOC): import React, {Component} from 'react' import axios from 'axios' const wrapWithAjax = (WrappedComponent, name) => { return class extends Component { constructor() { super() this.state = { lists: []}} componentWillMount() {const data = await axios.get(name)// Data})} render() {return (<WrappedComponent lists={this.state.lists} />)}} B/D: Import React from 'React' import wrapWithAjax from './wrapWithAjax' class B extends Component {render() {// render return () <> {this.props.lists. Map (item) => ()} </>)}} B = wrapWithAjax(B, 'XXX/XXXX ') export default B Copy codeCopy the code

Using the above example, it is clear that discovering a functional component’s custom hook uses less code, is more intuitive, more readable and easier to understand than HOC. Moreover, by observing the HOC code, a HOC is equivalent to making a layer of proxy for the original component, so there is no way to avoid the emergence of ‘nested hell’.