Written in the beginning

  • Before you read: I hope you know the use of basic hooks such as useCallback,useReducer, etc. I won’t cover the hooks that appear in this article too much.
  • Comment issues: For clarity, in some React code snippets I add comments to explain things. In JSX I use // instead of {/* */} for easy writing. Please ignore this error.

Start with a simple useToggle

If you’ve all used checkboxes or switch components, let’s implement useToggle() to make things even easier

function App() {
  const [on, toggle] = useToggle();
  // On is the state and toggle is the state. }Copy the code

Simple implementation

import React from 'react';
export function useToggle(on: boolean) :boolean, () = >void] {
  const [_on, setOn] = React.useState(on);
  return [_on, () = >{setOn(! _on)}] }Copy the code

Now the useToggle can be put into use. Have you noticed the small problem? Every time the on state changes, it causes us to return to the new toggle method, but it doesn’t really matter if we simply use it.

Try to optimize useToggle

Now that we have a new requirement, try optimizing useToggle to implement it.

Such as:

function App() {
  const [on, toggle] = useToggle();
  return(
    <div>NeedOn requires only the ON state<NeedOn on ={on}></NeedOn>// The Button component is only responsible for changing the state of on<Button toggle = {toggle}></Button>
    </div>)}Copy the code

In the example above, we have two components. NeedOn only needs to use the ON state, and Button only modifies the ON state.

Now every change in ON causes the App to re-render, which in turn causes NeedOn and Button to re-render. Now we need to make the Button no longer re-render when on changes with the same functionality.

How to solve it? First we need to solve the problem of toggle changing, because one of the basic conditions for a component not to re-render is that its props don’t change. We can use useCallback() to solve this problem.

import React from 'react';
export function useToggle(on: boolean) :boolean, () = >void] {
  const [_on, setOn] = React.useState(on);
  const _toggle = React.useCallback(() = >{ setOn(! _on); }, [])return [_on,_toggle];
}
// Now we use useCallback to cache _toggle.
// Since the passed array is empty _toggle will never be updated again, now we don't have to worry about the Button component doing extra rendering.
// -> -> -> If you are satisfied with this code, you may need to relearn the hooks
Copy the code

The above code does not have a rendering problem, but there is a more serious problem: the code logic is wrong

Let’s test it out a little bit.

The online test

// Test code, can skip
// Default on to true, call toggle 4 times, expect result :[true, false, true, false, true]
function useToggle(on = true){
  const [_on, setOn] = React.useState(on);
  const _toggle = React.useCallback(() = >{ setOn(! _on); }, [])return [_on, _toggle];
}
const values = [];
const App = () = > {
  const [on, toggle] = useToggle(true)
  
    const renderCountRef = React.useRef(1)
    
    React.useEffect(() = > {
      if (renderCountRef.current < 5) {
        renderCountRef.current += 1
        toggle()
      }
    }, [on])

    values.push(on)
    
    return null
}
setTimeout(() = > {console.log(values)},1000);
ReactDOM.render(<App/>.document.getElementById('root'))
Copy the code

We expect the result to be [true,false, true,false, true], but the actual result is: [true,false,false]. Let’s focus on the logic errors and ignore the length anomalies of the results for now.

Logic error caused by Capture Value

This logic error is due to the hooks capture Value feature, which is a bit like a JS closure. You can assume that each time a component renders, it is a separate snapshot, with its own “scope”.

function Count(){
  //count is a constant, each render count is independent
  // First click,count:0
  // Second click,count:1
  // Third click,count:2
  const [count,setCount] = React.useState(0);
  setTimeout(() = > {
    console.log(count);
    // Always print the corresponding value instead of the latest value
    // if you click the button three times in 2 seconds before the callback is triggered, the next three callbacks are triggered in sequence: 0,1,2 instead of 2,2,2
  },2000)
  
  return (
    <div>
      <span>{count}</span>
      <button onClick= {()= >{setCount(count + 1)}}> Increment?</button>
    </div>)}Copy the code

Now, we know what caused the error: since the _toggle method is not updated, the method references the external constant on to the default value true, and all subsequent calls to _toggle repeatedly change true to false.

So what’s the solution? Pass in the correct dependency in useCallback?

import React from 'react';
export function useToggle(on: boolean) :boolean, () = >void] {
  const [_on, setOn] = React.useState(on);
  const _toggle = React.useCallback(() = >{ setOn(! _on); },[_on])// Pass _on in the array so that every time _ON changes,_toggle changes too.
  return [_on,_toggle];
}
Copy the code

That way, it’s essentially the same as the way we started, and when the on changes, it also returns the new toggle.

Update state using useCallback and functions

You just need to use the function in useState to update:

import React from 'react';
export function useToggle(on: boolean) :boolean, () = >void] {
  const [_on, setOn] = React.useState(on);
  const _toggle = React.useCallback(() = > {
    setOn(_on= >! _on);// Now we pass the function in setOn. The _on in the function is updated every time.
    // Also, the dependency array is empty, which means that _toggle is not updated.}, [])return [_on,_toggle];
}
Copy the code

Now that we have written a nice hooks, we use useCallback to cache toggle so that when on changes, the toggle does not change, the Button component props does not change, and the Button does not re-render.

Using useReducer

We all know that the useReducer dispatch does not change, so we can use the useReducer internally in useToggle to return the dispatch.

import React from 'react';
export function useToggle(on = true) {
  function reducer(state,action){
    switch(action.type){
      case 'toggle': return! state;default: throw new Error();
    }
  }
  const [_on, dispatch] = React.useReducer(reducer,on);
  return [_on,dispatch];
} 
Copy the code

Is optimization done?

Unfortunately, optimizing useToggle alone doesn’t help. Online code: Optimize useToggle

Because when the parent component renders, the child component must be rerendered, regardless of whether the props of the child component changes.

So in addition to optimizing useToggle, we also cache Button, using useCallback’s cousin useMemo.

function App() {
  const [on, toggle] = useToggle();
  const MyButton = useMemo(() = > {
    return <Button toggle = {toggle}></Button>}, [])return(
    <div>NeedOn requires only the ON state<NeedOn on ={on}></NeedOn>{MyButton} {MyButton} {MyButton}</div>)}Copy the code

Now that we have completed the initial requirements, let’s review the steps:

  • We optimized useToggle to cache the internal toggle function with useCallback so that when on changes, the toggle does not change.
  • Based on the optimized useToggle, we also used useMemo to cache the Button, so that when on changes, the App will re-render, but the Button will not be re-rendered.

Online code: optimization completed

UseMemo before

In most cases, you don’t need to do this because it doesn’t help with performance cues, and frequent use of useMemo and useCallback can be mentally taxing. So before you use useMemo to reduce repeated rendering of a particular component, think about whether you need to use it.

The following example may appear in your code.

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}

function App() {
  const [on, toggle] = useToggle();
  return(
    <div>NeedOn requires only the ON state<NeedOn on ={on}></NeedOn>// The Button component is only responsible for changing the state of on<Button toggle = {toggle}></Button>
      <ExpensiveTree/>
    </div>)}Copy the code

The difference is that our goal now is to prevent ExpensiveTree from re-rendering. Simply using useMemo will do the trick. But what else?

Sinking the state

ExpensiveTree re-render because App re-render, then we try to avoid App re-render directly.

App rerenders because of the on(state) change. So we can just pull NeedOn and Button apart, or put useToggle(useState) into the child component.

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}
function Toggle(){
  const [on, toggle] = useToggle();
  return(
    NeedOn requires only the ON state
    <NeedOn on ={on}></NeedOn>
    // The Button component is only responsible for changing the state of on
   <Button toggle = {toggle}></Button>)}function App() {
  
  return(
    <div>
      <Toggle/>
      <ExpensiveTree/>
    </div>)}Copy the code

Toggle now rerenders, App and ExpensiveTree don’t.

Enhance the content

But in this case, we don’t seem to be able to sink the useToggle. Because we’re going to change styles based on the state of on.

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}
function App() {
  const [on, toggle] = useToggle();
  return(
    // Now we need to change the style according to the state of on
    <div style={on ? {color: 'red'} : {color: 'black'} >
      <NeedOn on ={on}></NeedOn>
      <Button toggle = {toggle}></Button>
      <ExpensiveTree/>
    </div>)}Copy the code

How do you do that?

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}
function Toggle({children}){
  const [on, toggle] = useToggle();
  return (
    <div className={on ? 'white' : 'black'} >
      <NeedOn on ={on}></NeedOn>
      <Button toggle = {toggle}></Button>
      {children}
    </div>)}function App() {
    return(
      <Toggle>
        <ExpensiveTree/>
      </Toggle>)}Copy the code

We pull out a Toggle component and do that by passing in ExpensiveTree.

Online code: Improve content

Because when on changes and Toggle re-renders, the ExpensiveTree we passed through the App will not change. Now we can both change styles via ON and avoid ExpensiveTree re-rendering.

Above, when using React, let’s take a look at some unnecessary “expensive” components and see if we can avoid them by doing some simple things.

The bail out causes the abnormal length

In the example above, the logic is broken due to the nature of Capture Value, but otherwise the length of the result [true,false,false] is somewhat different from the actual value.

Bailing out of a state update.

That is, if your updated state is “the same” as the current state, it will cause Bail out, descendants of the component will not be re-rendered and useEffect will not be triggered.

Object. Is (nextState,curState) returns true. Object.is is a shallow comparison.

Review the above test code.

function useToggle(on = true){
  const [_on, setOn] = React.useState(on);
  const _toggle = React.useCallback(() = >{ setOn(! _on); }, [])return [_on, _toggle];
}
const values = [];
const App = () = > {
  const [on, toggle] = useToggle(true)
  
    const renderCountRef = React.useRef(1)
    
    React.useEffect(() = > {
      if (renderCountRef.current < 5) {
        renderCountRef.current += 1
        toggle()
      }
    }, [on])
    // On initialization, push true
    UseEffect executes after initialization -> triggers toggle for the first time and changes on to false
    // useEffect is executed after toggle is triggered for the first time. When toggle is triggered for the second time, react checks that Object(false,false) is true and bail out.
    values.push(on)
    
    return null
}
setTimeout(() = > {console.log(values)},1000);
ReactDOM.render(<App/>.document.getElementById('root'))
Copy the code

Why is useEffect executed twice and values.push pushed three times?

I don’t know if you have this question, first of all, this question is actually mentioned above:

If your updated state is “the same” as the current state, this will cause bail out, descendants of this component will not be re-rendered and useEffect will not be triggered.

To explain react in detail, we need to go a little further. We should know that Currently React uses Fiber architecture, and fiber update is divided into two stages: Render and Commit. For class components most life cycles fire in the Commit phase (life cycles with will fire in Render). For hooks,useEffect (including useLayoutEffect) also fires in the Commit phase. During the Render phase we compared the JSX object to the old Fiber and recorded the changes in the effectList. In order to ensure that the update is actually bailed out, when react reRedener is executed, the update should be bailed out if only one setState is checked. These setStates are stored chained in the Update Ue property of the Fiber node. React calculates the final state in the Update Ue chain during the Render phase and stores the result in Fiber’s memoizedState property. Only at this point can we make a comparison and decide whether we should bail out.

In our test code, values.push fires in the render phase, so it fires three times, while useEffect is not in the render phase and does not fire a third time.

Here are two links to help you understand the problem.

Why React needs another render to bail out state updates?

useState not bailing out when state does not change #14994

reference

www.developerway.com/posts/how-t…