preface

It’s been over two years since the React Hooks were officially released in version 16.8.0, and I’m sure every developer has been caught in the trap, and most are in widespread use. However, just by observing the code in several teams in our company, I found that the usage of the code is very strange, and there are many cases of misuse and abuse, and I don’t understand it very well. I’ve compiled a few for everyone to appreciate and avoid stepping in pits;

Common Code Problems

Example 1: Hooks function order

We all know that React Hooks rely on fixed order calls when rerendering. Within a function component, you can call as many hooks APIS as you like.

const Demo = () = > {
  const [a,setA] = useState('aaa');
  const [b, setB] = useState('bbb');
  
  // useState is used in conditional statements
  if(type) {
    const [c, setC] = useState('bbb');
  }
  
  return <div>Components in the sample</div>
}
Copy the code

Of course, under the official website, generally no one write this code. But some people write code that looks like this:

/ / simplified version
const Demo1 = (props) = > {
  
  const renderInfo = () = > {
    const {name} = props || {};
    / /... Logic code
    return useMemo(() = > {
      return (
        <div>Name: {name} {/*<Component1 />* /}</div>
      )
    },[]);
  };

  return (<div>
    <h4>Components in the sample</h4>
    <p>The user information</p>
    {renderInfo()}
  </div>)}Copy the code

In a complex component, the original intention is to follow the split thinking of class components, extract renderInfo in block split mode, and automatically use useMemo to reduce internal rerendering. At first glance it looks fine, and the code works fine. But there are two big problems: 1) useMemo optimization doesn’t work; The component is rerendered, and the renderInfo function is actually recreated. 2. Introducing hidden bugs; A new colleague, Xiao Wang, receives a new demand: in the visitor mode, this content is not displayed; Naturally, the following code was added:

const Demo1 = (props) = > {
  const renderInfo = () = > {
    const {name} = props || {};
    / /... Logic code
    return useMemo(() = > {
      return (
        <div>Name: {name} {/*<Component1 />* /}</div>
      )
    },[]);
  };

  return (<div>
    <h4>Components in the sample</h4>{props.type! =='visitor' && (<><p>The user information</p>{renderInfo()}</>)}
  </div>)
}
Copy the code

Such code, when type=’visitor’, will cause React processing to fail and crash and exit; Especially in complex components, suddenly hidden such a piece of error code, do not know when the bomb will explode. The hook API can only be used at the top level of function components.

React batch update mechanism

Example 2: In async, settimeout and other functions, update multiple state data without merging the update problem;

React has a batch update mechanism. Normally, multiple useState updates from multiple setStates in a Class component or multiple useState updates from a Function component are merged into a single operation. Reduce unnecessary repetitions. In functional components, there is also an optimization strategy to update multiple setstates, merge updates, and trigger a rerendering. Here’s an example:

export default function App() {
  const [a, setA] = useState("");
  const [b, setB] = useState("");
  const [c, setC] = useState("");

  const numRef = useRef(0);
  console.log('Render times performed:${numRef.current}`);

  const onClick = () = > {
    setA((old) = > old + "a");
    setB((old) = > old + "b");
    setC((old) = > old + "c");
    numRef.current += 1;
  };

  return (
    <div className="App">
      <h1>Hello developer!</h1>
      <br />
      <div>
        <Button type="primary" onClick={onClick}>Click on the button</Button>
      </div>
    </div>
  );
}

Copy the code

Example – Portal

In the example above, three states are updated after the button is clicked, but the rerender is triggered only once.

Legend:





However, in async, setTimeout, and other functions, the picture is different:

export default function App() {
  const [a, setA] = useState("");
  const [b, setB] = useState("");
  const [c, setC] = useState("");

  console.log('Component rendering', a, b, c);

  // Update multiple states in asynchronous code
  // const onClick = async () => {
  // await 1;
  // setA((old) => old + "a");
  // setB((old) => old + "b");
  // setC((old) => old + "c");
  // };

  // Update multiple states in setTimemout
  const onClick = () = > {
    setTimeout(() = > {
      console.log("setTimemout");
      setA((old) = > old + "a");
      setB((old) = > old + "b");
      setC((old) = > old + "c");
    }, 1000);
  };

  return (
    <div className="App">
      <h1>Hello developer!</h1>
      <br />
      <div><Button type="primary" onClick={onClick}>Click on the button</Button></div>
    </div>
  );
}
Copy the code

Example – Portal

React does not merge updates after multiple component renderings are triggered by clicking the button.

Legend:

So what is the difference and the reason for it? The difference between the above two examples is that the js engine executes the update in an EventLoop asynchronous queue. React cannot actively intervene in the subsequent operations, but only passively execute them one by one. However, in real projects, we often need to update state in asynchronous callbacks such as async await or Promise. Be careful when updating multiple states and performing side effects on multiple states as dependencies.

useEffect(() = > {
  // Request the interface data
  fetch({a,b,c});
},[a,b,c]);
Copy the code

In the above code, changes in A, B, and C all trigger requests, resulting in two redundant requests, inconsistent interface processing speed, and easy to cause page data disorder. The above is just a common scenario, but real development scenarios for performing the same side effect on multiple dependencies are many and more complex. If not handled well, it is easy to cause bugs; There are ways to deal with it:

  1. Handle the sequential relationship of multiple state updates by itself, and add restrictions on the execution of side effects;
  2. Use useReducer to gather the dependent state changes;

Example 3: Reference type update shallow comparison problem

Shallow comparison problem setState; Problems that arise when complex array objects are changed

var a =[1.2.3]

a.push(4)
setA(a);
Copy the code

In react, useEffect, useCallback, useMemo and other dependencies are shallow comparisons; Pay attention to this! For complex objects where only certain properties are used, it is perfectly possible to add only the corresponding properties to the dependencies:

useEffect(() = >{... },[info.name, info.age])Copy the code

Example 4: Asynchronous updates – race issues;

For example, multiple scenario changes in a page trigger the same asynchronous request to update the data. If the second asynchronous request is returned before the first asynchronous request, a race problem occurs. The page renders mismatched data. One way to solve the race problem is to add a flag: code:

useEffect(() = > {
  let isCancel = false; // Cancel the asynchronous request processing status
  // Obtain data asynchronously
  const qryData = async() = > {try {
      const params = {a, b};
      const res = await fetch({ url: API_MESSAGE, payload: params });

      if(! isCancel) {// If there is a race, the data is not updated. Otherwise update the data
        curDispatch({ type: 'list-data'.payload: list || [] }); }}catch (err) {
      console.warn('Interface processing failed,', err); }}; qryData();return () = > {
    isCancel = true;
  };
}, [a, b]);
Copy the code

UseEffect dependency problem;

The misuse of useEffect and useCallback dependencies is a major source of bugs in projects. Here are some guidelines:

  1. Don’t lie to React about dependencies; Add all dependencies;

    The official documentation also requires that we put all the data streams used in effect into dependencies, including state, props, and component functions.

  2. When you add too many dependencies, say, a dozen, you have to rethink whether your state split and component split are unreasonable.

Specific Hook API use encountered some scenarios

useState

  1. To reduce the cost of understanding for other developers on the team, the useState variable is placed at the top of the function component; It is better to add comments;
  2. Try to extract the state to the upper component, public state extracted to the public parent component;
  3. Keep any data from one source and avoid copying it directly or arbitrarily deriving state. Many scenarios can be resolved by passing props or useMemo.
  4. State Split granularity:
    1. When there are too many state states, or the state association changes. This can be put into a state object based on the correlation of data states.
    2. For complex state processing, useReducer is recommended.
      1. The page defines a bunch of states;
      2. For example, if a changes, b and C need to be changed;

useEffect

We use useEffect to do side effects; Is one of the most commonly used Hook apis.

The useEffect dependency problem

React uses useEffect for side effects. The function assigned to useEffect is executed after each render round and when the dependencies passed in are changed. As mentioned earlier:

A function component is first a normal function that is executed every time it is rendered. Each execution of a function generates its own execution context. Accordingly, React rerenders components with its own variables and functions, including Props and states, and its own event handlers. Second, React HooksAPI gives unique meaning to some variables wrapped by HooksAPI in functions: cached values and functions, value changes trigger rerendering, etc.

An Effect belongs to a particular render, and each Effect function “sees” props and states from the particular render it belongs to. The effect dependency determines whether the passed function is executed. This is why it is important to add all dependencies to ensure that the proper props and state values are obtained within the effect. Don’t try to trick React, it may cause bugs. Is it safe to omit functions from the dependency list? Closures cause variable fetching to be delayed – links

useCallback

UseMemo, useCallback exist as a way to optimize performance, not as a semantic guarantee to prevent rendering;

There is no need to wrap all the normal functions defined within a component in useCallback; overuse can introduce some strange bugs.

The principle is: don’t use it if you don’t know.

In addition, the following scenarios can help improve performance:

1. Reduce unnecessary rerendering of subcomponents;
/ / child component
const Child = memo((props:any) = > {
  console.log('Subcomponent rendering... ');
  return (<div>Subcomponent rendering...</div>);
});

/ / the parent component
const Parent = () = > {
  const [info, setInfo] = useState({});
  const [count, setCount] = useState(0);
  const changeName = () = > {
    console.log('Changed the information... ');
  };

  return (
    <div className="App">
      <div>Identification: {count}</div>
      <div onClick={()= >{ setCount(count => count + 1); }}> Click Add</div>
      <Child info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;
Copy the code

For example, in the example above, a change in count causes the Parent component to rerender, but triggers the Child component to rerender. The reason is that the parent component is re-executing, the parent component is re-generating the new changeName function, and the child component is passing it props, triggering a re-rendering. In normal usage, this is fine. However, in scenarios where performance is high or rerendering within a child component is too expensive, you can avoid this unnecessary rerendering by modifying the parent’s changeName method and wrapping a layer with useCallback hook functions.

  1. If a child component uses the functions passed in to it as dependencies, such as effects, it can cause bugs such as infinite loops.
/** bad case **/
let count = 0;

function Child({val, getData}) { 
  useEffect(() = > { 
    getData();
  }, [getData]);
  return <div>{val}</div>
}

function Parent() { 
  const [val, setVal] = useState(' ');
  function getData() {
    setTimeout(() = > {
      setVal("new data " + count); 
      count++;
    }, 500); 
  } 
  return <Child val={val} getData={getData} />; 
} 

export default Parent;
Copy the code

In the code above, the Child component useEffect retrieves data from getData. The actual situation is that getData is regenerated every time val changes in the Parent component trigger a rerendering, resulting in an infinite loop. The solution is to cache the getData reference by wrapping the getData function with useCallback.

useRef

In general, useRef can be used in two scenarios:

1. Point to the COMPONENT DOM element

A. Obtain the attribute values of component elements. B. An API to manipulate the target to point to the DOM, such as an input element in the following example, and focus the input after the page is rendered;

const Page = () = > {
  const myRef = useRef(null);
  useEffect(() = > {
    myRef.current.focus(); // Target input focus
  });
  return (
    <div>
      <span>UseRef:</span>
      <input ref={myRef} type="text"/>
    </div>
  );
};

export default Page1;
Copy the code

2. Store variables

You can store any variable value and the value will not go into the dependency collection item; Similar to the way the class component uses instance fields, similar to this, when rerendered, the same reference is returned each time;

const Page = () = > {
  const myRef = useRef(0);
  const [list, setList] = useState([])

  const onDelClick = () = >{}const onAddClick = () = >{}return (
    <div>
    <div onClick={()= >SetCount (count + 1)} > click count</div>
<div onClick={()= >SetCount (count + 1)} > click count</div>
<div onClick={()= >HandleClick ()} > view</div>
</div>
);

export default Page;
Copy the code

UseMemo usage appreciation

UseMemo can be used in the following scenarios:

  1. Cache complex calculated values to reduce unnecessary state management;
export default Demo = ({info}) = > {
  const money = useMemo(() = > {
    // Calculate the render value
    const res = calculateNum(info.num);
    return res;
  },[info.num]);
  
  return <div>Price is: {money}</div>
}
Copy the code

In this code, the money field can be cached by useMemo. Only changes to info.num will be recalculated, reducing unnecessary computations and avoiding unnecessary maintenance of derived states.

  1. Cache parts of JSX or components to avoid unnecessary rendering;
export default Demo = ({info}) = > {
  const topEl = useMemo(() = > (
    <div>
    	<p>The user information</p>{/* render user data... * /}</div>
  ),[info.user]);
  
  return <div>{topEl} {/* render list data... * /}</div>
}
Copy the code

The above code can be used to cache the corresponding JSX while part of the state data is unchanged. First, the complex logic can be split properly to make the components more concise; If the processing logic is complex enough, it is recommended to separate the components from each other and package the sub-components with the Memo.

Wrong usage:

You can put theuseMemoAs a means of performance optimization, but don’t think of it as a semantic guarantee.

  1. Example 1:
const Demo = () = > {
  const [name, setName] = useState(undefined);
  const [copyNum, setCopyNum] = useState(0);

  // Control the display value
  const topEl = useMemo(() = > (
    <div>The copied value is: {name}</div>
  ), [copyNum]);

  return (
    <div className="page-demo">
      <Input value={name} onChange={(e)= >SetName (e.target.value)} placeholder=" Please enter name "/><Button onClick={()= >SetCopyNum ((old) => old+1)}> copy</Button>
      {topEl}
    </div>
  );
};
Copy the code

In the simplified version of the example above, the intention is to copy the current value in the input box when the “Copy” button is clicked. In the code, you want to use useMemo to control the display results. TopEl uses the name, but does not add to the dependency, only click the button, copyNum changes, the display content name will change. It looks fine, but you’re using useMemo in the wrong place. That is, useMemo is used to control the rendering result, which guarantees the result value semantically, rather than optimizing the performance. What’s the problem with that? This can lead to mismatches between state values and render values, causing confusion and bugs.

  1. UseMemo abuse;
const Demo = () = > {
  const [name, setName] = useState(undefined);
  const [copyNum, setCopyNum] = useState(0);

  // Control the display value
  const topEl = useMemo(() = > <div>The copied value is: {name}</div>, [name]);

  return useMemo(() = > (
    <div className="page-demo">
      <Input value={name} onChange={(e)= >SetName (e.target.value)} placeholder=" Please enter name "/><Button onClick={()= >SetCopyNum ((old) => old + 1)}> copy</Button>
      {topEl}
    </div>),[name, age, num,topEl,...] ); };Copy the code

The above example, I found many developers in the project write this, intended to the entire component return JSX cache optimization. However, there are several problems: 1. After complex components, there are too many dependencies. Each state, useMemo, or useCallback must be added to the dependencies manually. Increase unnecessary maintenance costs and error probability. 2. When the component performs rerendering, it wants to have the corresponding JSX, instead of caching the entire component’s returns semantically. In one case, caching the entire component’s state changes is equivalent to not doing it. The parts that need to be optimized for caching can be extracted into individual uesMemo sections; Return only does combinations. If we encounter component modification, we need to add components to return in advance to reduce the rendering of sub-components, which will directly cause bugs; Ex. :

.if (loading) {
  return <Loading />
}
return useMemo(() = > (<div>.</div>), [name, ...] );Copy the code

The better logic is to split the useMemo cache into multiple useMemo caches based on the actual business logic in a scenario that really needs to be optimized to avoid unnecessary rerendering of subcomponents:

// According to the page function module split, processing into different logical units;
const topEl = useMemo(() = >(<div>.</div>),[topInfo]);
const userEl = useMemo(() = >(<div>.</div>),[userInfo]); 

if (loading) {
  return <Loading />
}
return (<div>
  	{topEl}
  	...
  	{userEl}
  </div>);
  
Copy the code

Of course, if there are still too many dependencies, consider using useReducer to fold the state. Logically complex components should be split into subcomponents first;

Use of useReducer

Don’t be afraid to use useReducer

  1. It is not as profound as you think, learning can solve a lot of practical problems;
  2. In the source code implementation, a lot of knowledge about Reducer and Dispatch is used. In essence, the implementation principle of useState and useReducer is similar. You can think of useState as a special useReducer; The difference from useReducer is that useState is provided with a predefined reducer handler;

UseState returns a reducer state and an Action Dispatcher.

Examples: To be added…

After the language

The preface should be supplemented by the epilogue

The better you understand the concept, the smoother it will be. What I am doing now is summarizing what I have seen and learned in the React practice process during continuous learning. This article is mainly about React Hooks. The pros and cons of the codes involved will be discussed in the next article.

  • React (1)- From simple to complex Concepts (completed)
  • (2)-React Hooks Practice (to be optimized)
  • (3)-React Hooks