React Hooks North

Time:2022-9-21

foreword

This article aims to summarize the use skills of React Hooks and the problems that need to be paid attention to in the process of use, which will add some reasons for the problems and solutions. However, please note that the solutions given in the article are not necessarily fully applicable. There are many solutions to the problem. Maybe your team has given corresponding specifications for these problems, or you have already solved these problems. solutions to form a better understanding. So your focus should be onAre you aware of these issues while using React hooks?as well asyour thoughts on these questions

useState hook

initialized state

If a certain state of the component needs to rely on a lot of calculations to get the initial value, generally we will define a function to initialize the state

  • in the class component

    state = {
     state1: calcInitialState()
    }

    No problem, calcInitialState will only be executed once even if the component is re-rendered multiple times without the component being remounted

  • In function components, there are two ways

    const state1 = useState(calcInitialState) // When the component is rendered multiple times, calcInitialState will only be executed once
    const state1 = useState(calcInitialState()) // every time the component renders, calcInitialState will be executed

    Each time the function component is re-rendered, the function component itself will be executed. When it is rendered for the first time, useState will read the initial value. If it is an initial value function, it will be executed, and the return value of the function will be used as the initial state. , the two expressions behave the same. However, in the subsequent re-rendering process, although useState will not read the default state value, nor will it do anything with the default state value, but the calcInitialState in the second writing method will still be executed, which is meaningless. See the source code mountState and updateState for the internal running process

How state is captured (this & closure)

Some time ago, I shared common React Hooks principles and source code within the team. At that time, I mentioned that whether it is a class component or a function component, their state is stored on the fiber corresponding to the component. The flow of status update of function components and class components is shown in the following figure:

React Hooks North

For details, see the source code updateClassInstance and updateReducer

For the rendered part (JSX), you can always get the latest state. Only the way to get the state is different. The class component points to the state stored on the fiber node through this.state, and the function component uses this.state to point to the state stored on the fiber node.useStateThe return value of this function is obtained, then the problem is that the state obtained by the function component is stored in the closure, which is defined byuseStateExecute produces.

On the other hand, for functional components, we need to pay special attention to the concept of "rendering". Every time the function component is rendered, the function declared inside or the returned UI (JSX) can only capture the props and state of the current rendering, which is no problem for UI (JSX). But for functions inside function components, especially functions with delayed callbacks, you need to pay special attention to whether the state and props captured in the callback function are what you expect when the callback function is executed.

To avoid the troubles caused by the closure problem in functional components, you need to understand and remember the following two sentences

  • Each render of a functional component has its own props and state
  • Each render of a function component has its own event handler

state granularity

State granularity is too fine

When writing class components, there is almost no need to consider the problem of state granularity, because developers can always declare all state at once or update all state at once, like this

handleClick = () => {
    this.setState({
    currentPage: 2,
    pageSize: 20,
    total: 100
  })
}

Most developers aren't stupid enough to use it three timessetStateto update these three states, but only in function components

const handleClick = () => {
  setCurrent(2)
  setPageSize(20)
  setTotal(100)
}

Seeing this code, um, may make people feel a little uncomfortable. The problem is that the update granularity is too fine. In fact, the currentPage, pageSize, and total of a paging component often need to be updated at the same time, but triggering setXXX multiple times is still Can make people feel vaguely uneasy, even if the update is triggered multiple times, React'sbatchUpdateMechanisms are merged into one, but multiple updates are triggered when setXXX methods are executed out of React's context, such as in callbacks at the end of async.

At this point, we can store an object in useState to keep the associated states together. You can also use useReducer to manage multiple states.

State granularity is too coarse

When a state has a certain complexity, I do not recommend violently inserting the state declaration of the class component into the useState, because this may introduce the defects of the coarse-grained state in the class.

Problem 1: It is difficult to find state logic that can be reused

When a component has more and more states, the readability and maintainability of the component will become worse and worse. Many people should have a deep understanding, like this:

ps : intercepted from real business code

class XXX extends React.Component{
  constructor(props: any) {
    super(props);
    this.state = {
      tableListMap: {},
      showPreview: false,
      showRegModal: false,
      dataSource: [],
      columns: [],
      tablePartitionList: [],
      incrementColumns: [],
      loading: false,
      isChecked: {},
      isShowImpala: false,
      tableListSearch: {},
      schemaList: [],
      fetching: false,
      tableListLoading: false,
      bucketList: [],
      showPreviewPath: false,
      previewPath: '',
      currentObject: { object: [''], index: 0, bucket: '' },
      isCompressed: false,
      matchType: null
    };
  }
}

Just imagine, in a function component, a useState is stuffed with so many states, not to mention whether you can find the reusable state and logic in it, even if you have a keen eye and find that it is compatible with other components Multiplexed state and logic. With a high probability, it is also difficult to extract the reusable state logic without guaranteeing that the current component (historical code) will not fail.

Problem 2: Putting unrelated states in the same useState can make state updates unmanageable

For example, if there is a button on the page, when the button is clicked, two pieces of data need to be obtained from different interfaces at the same time and rendered on the page. At this time, if the two pieces of data are stored in the same useState

function DataViewer (props) {
    const [dataMap, setDataMap] = useState({ data1: undefined, data2: undefined })
  
  const loadData1 = async () => {
    if (visible) {
      const data1 = await fetchData1()
      setDataMap({ ...dataMap, data1 })
    }
  }

  const loadData2 = async () => {
    if (visible) {
      const data2 = await fetchData2()
        setDataMap({ ...dataMap, data2 })
    }
  }
  
  const handleClick = () => {
    loadData1()
    loadData2()
  }
 // ...
}

The problem is obvious, as long as both requests complete, no matter whether it succeeds or fails, there will be no data in the dataMap returned by the request that completed first.

In the class component, this problem basically does not occur, because the current latest state can always be obtained through this, and there will be no problem of state overwriting in multiple updates. Of course, in the function component, you can also use useRef to temporarily store the interface data, and then update the state together, but you need to write some extra logic, so this black technology will not be introduced here.

The first solution at this time is to temporarily store the data returned by the first interface in a variable and wait until the second interface is completed before updating the dataMap, like this

const loadDataMap = async () => {
  if (visible) {
     const data1 = await fetchData1()
     const data2 = await fetchData2()
     setDataMap({ data1, data2 })
  }
}

The problem with this is that it is necessary to wait for the completion of the first request before initiating the second request, which is not friendly to the user experience.

Well, if you want two interfaces in parallel, you can also use Promise.allSettled to process two requests in parallel

const loadDataMap = () => {
  if (visible) {
     Promise.allSettled([ fetchData1(), fetchData2() ])
        .then(results => {
        //...
     })
  }
}

It looks like there is no problem. However, if there is additional processing logic for the state, return value, etc. of the interface, you need to stuff all the processing logic of the interface into the .then callback, and this method must be updated after both interfaces are completed. The state then displays the data on the page, and it is impossible to detect whether one of the interfaces is in the pending state. This method does not seem to be so friendly.

It seems that there are only two perfect solutions

  1. To avoid closures, you just pass in a function when updating the state, like this

    setDataMap(dataMap => ({ ...dataMap, data2 }))
  2. state segmentation

    const [ data1, setData1 ]  = useState()
    const [ data2, setData2 ]  = useState()

There is often more than one way to solve the problem. You need to choose the way you think is more appropriate according to the actual business situation.

How to design state granularity

It is said in the QA of the official document

put all state in the sameuseStateIn the call, or each field corresponds to auseStateCall, these two methods can run through. Components are more readable when you find a balance between these two extremes and combine related state into several separate state variables. If the state logic starts to get complicated, we recommend using a reducer to manage it, or using a custom Hook.

In my opinion, it is a good practice to aggregate related states and split unrelated states. For example, the three states of currentPage, pageSize, and total of the pager component are placed in the same useState, and the returned data of different requests is split into different useStates. In addition, in some cases, the logic of the state is relatively complex. At this time, useReducer can also be used to manage the state, so that some complex logic can be extracted into the reducer.

Two ways of status update

Whether it is a function component or a class component, there are two ways to update the state:setState(newState)andsetState(oldState => newState), the difference between them is that one focuses on results and the other focuses on goals.setState(newState)is used to describe the new state, whilesetState(oldState => newState)Used to describe what changes should be made in the new state compared to the old state.

It may be a bit abstract to say this, in simple termssetState(newState)is to replace the old state with the new state,setState(oldState => newState)is used to calculate the new state from the old state

useEffect hook

Why useEffect is needed

In theory, function components are simply used for rendering, which is the so-called pure function. In fact, this was the case before React Hooks. And other operations such as data acquisition, setting timers, modifying the DOM, etc. are called side effects.

Why can't side effects be performed directly inside a function component?

  • There are some side effects operations that may affect rendering, such as modifying the DOM
  • There are some side effects operations that need to be cleared, such as timers
  • If side effects are performed directly inside the function component, then the function component will perform these operations every time it is re-rendered, and there is no way to control when these operations are performed and when they are not performed.

How does useEffect solve these problems?

  • The function wrapped by useEffect will be executed after the browser rendering is completed, ensuring that it will not affect the rendering of the component. In addition, the execution of the function wrapped by useEffect is out of the execution context of the function component itself, so it will not affect the execution of the function component itself.
  • A function wrapped in useEffect can return a function that clears side effects
  • useEffect can pass in an array of dependencies, and only perform side effects when the dependencies change

These features of useEffect are a bit like event callbacks, except that the triggering of event callback functions depends on dom events such as clicks, input, etc., while the functions wrapped by useEffect depend on changes in dependencies. In many cases, it is a better choice to put some side effect operation into the event callback function, so that you can not worry about useEffect dependencies.

How useEffect captures props and state

In the function wrapped by useEffect, the way of capturing props and state is the same as that of ordinary functions, depending on the execution context of the function component itself. UseEffect doesn't do anything special inside like data binding, fiber dependencies, etc. therefore,Each render of a functional component has its own effects. After the rendering of the function component is completed, the generated effects will be stored on the fiber corresponding to the component, waiting for a specific timing to execute these effects (side effects). Even if the page has been re-rendered many times when some asynchronous callbacks in effects are executed, the props and state captured in these asynchronous callback functions are still the state and props of the component in the rendering that produced these effects.

Dependencies for useEffect

Which should be placed in useEffect's dependencies

In theory.useEffectmental model is closer toeffect to execute when some value changes, but sometimes in order to ensure that the props and state captured in the effect are what you expect, you have to put all the variables in the component used in the effect into the dependencies. If you set the corresponding lint rules in the project, the lint tool will also tell you that you should do this, but this seems to be the same asuseEffect's mental model created some conflicts.

The consequence of this conflict is that the effect may be executed frequently, the following example is a counter that increments every second

function Counter () {
  const [count, setCount] = useState(1)
  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(timerId);
  }, [count]);
  
  return (
      <>{count}</>
  )
}

If the count in the dependency of this useEffect is removed, the callback function in the timer will always be executedsetCount(1 + 1), which is not what we expected. Well, to be honest, put count in a dependency, but then the timer will be cleared and created frequently, which may affect the frequency of the timer callback firing, which is not what we expect.

Until now, the problem has not been solved, I still tend touseEffect's dependencies are used to trigger effects, instead of solving the closure problem, you can only find a way to remove the useEffect's dependence on count.

How to reduce useEffect's dependencies

  • eliminateuseEffectUnnecessary captures in
    The useEffect in the above example can be written as

    useEffect(() => {
      const timerId = setInterval(() => {
     setCount(count => count + 1);
      }, 1000);
      return () => clearInterval(timerId);
    }, []);
  • Decouple dependencies from effects
    Or the example of the timer counter, if we want to pass a step property to the component through props, which is used to tell the component the size of the value to increment every second
<Counter step={2}/>

So the Counter component becomes like this

function Counter ({step}) {
  const [count, setCount] = useState(1)
  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(count => count + step);
    }, 1000);
    return () => clearInterval(timerId);
  }, [step]);
  
  return (
      <>{count}</>
  )
}

The problem now is that when the value of step changes, the timer is still restarted. nowuseReducerPlayed.

function Counter ({step}) {
  const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {
    if (action.type === 'tick') {
      return state + step;
    } else {
      throw new Error('type in action is not true');
    }
  }

  useEffect(() => {
    const timerId = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(timerId);
  }, []);
  
  
  return (
      <>{count}</>
  )
}

So now the question comes:
Q: _Why can we not include in dependencies_dispatch_ ?_
A: Because React promises us_dispatch_ (from useReducer)setState (from useState)as well asref.current (from useRef)Even if components are rendered multiple times, their reference addresses do not change
Q: _Why is the step value captured in the reducer the latest? _
A: ForuseReducerIn other words, React only remembers its actions, not its reducers. That is to say, every time the component re-renders and executes useReducer, it will re-read the reducer
said aboveuseStateanduseReducerIt's the same thing in the source code. In the actual use process, compared to useState, useReducer allows us to putUpdate logic (reducer)andDescribe what happened (action)separate, which can be used to remove unnecessary dependencies.

Of course, in actual business, the scenarios in the above examples are rarely encountered. In most cases, only theThe value used to trigger the execution of the effectinto useEffect's dependencies. For scenarios that cannot be solved by the above two methods, useRef can also be used to bypass the annoying closure problem.

Should functions be used as dependencies of useEffect

Personal opinion is:In the vast majority of cases, functions should not be used as dependencies of useEffect.As for whether it is safe to remove functions from dependencies, it mainly depends on

  • Whether the function participates in React's data flow
    In short, it is to see if the variables inside the function component (except useRef) are used in this function. If a function does not participate in the React data flow, butuseEffectused in, you should extract this function outside the component, so you canuseEffectBrainless remove this function from the dependencies.
  • whether the function is called asynchronously and lazily
    When the function is called late, it is easy to cause the closure problem. At this time, even if the function is used as a dependency of the useEffect, the closure problem cannot be solved, but it may increase the trigger frequency of the effect. The following uses variable data instead of immutable data The data section will introduce a method to ensure that the function automatically captures the latest value of the variable in the component when the function reference address remains unchanged.

Most of the time it's fine to not put the function in useEffect's dependencies. There may be some extremely special business scenarios. In this case, the function can only be wrapped with useCallback and then placed in the dependencies of useEffect.(I haven't encountered this situation so far)

useRef hook

According to my personal understanding, useRef is more like an instance attribute of a class component, that is, this.xxx. In the function component, useRef can be regarded as a container, you can manipulate the data in this container arbitrarily, and the reference address in this container will not change due to multiple re-rendering of the component. In my opinion, it is a cheater for function components and a unique tool for solving the closure problem in function components.

Features of useRef

  1. When the value stored by useRef changes, it does not cause the component to re-render
  2. It can be used to store variable data. When the component is rendered multiple times, the reference address of ref (the return value of useRef) itself can remain unchanged.

ps: The reason why useRef can ensure that the reference address of the return value remains unchanged is that even if the component is rendered multiple times, the ref returned by useRef is the ref returned when it is executed for the first time. When the function component is rendered for the first time, React will store the return value (ref) of useRef on the fiber node corresponding to the component. When the subsequent component is re-rendered, React will not do any processing on the useRef and directly return to the fiber node. Stored ref. See the source code for detailsmountRefandupdateRef

What useRef-based traits can do

  • Implement a custom hook to count the number of renders of the component

    const useRenderTimes = () => {
      const ref = useRef(0)
      ref.current += 1
      return ref.current
    }
  • Record a value from the last time the component was rendered

    const usePreValue = (value) => {
      const ref = useRef(undefined)
      const preValue = ref.current
      ref.current = value
      return preValue
    }
  • Avoid unnecessary re-renders
    If a state has nothing to do with rendering, then you can use useRef instead of useState. Remember the Counter component in the useEffect section above, if count is used as a dependency of useEffect, then the timer will be created/destroyed continuously, two solutions are given above, now let's talk about another Way. The idea is that as long as the count changes, the component is not re-rendered, then useRef can be used to store the count value. Of course, this method is only limited to the case where the count does not participate in the rendering.Or you can trigger the component to re-render when the value stored in useRef changes
    Such scenarios are actually very common. For example, there is a form. When the user completes the form, click the submit button to send the data to the backend through the interface. If the form is not a controlled component, then using useRef to store the form data is a better choice than useState, becauseit doesn't cause unnecessary re-renders

    function Counter () {
      const count = useRef(1)
      useEffect(() => {
     const timerId = setInterval(() => {
       count.current += 1;
     }, 1000);
     return () => {
       clearInterval(timerId)
       console.log(count.current)
     };
      }, []);
    }

Sometimes useState is used in function components to still get a certain value after the component is re-rendered, but we want to not trigger component updates when this value changes, or we want to avoid the closure problem caused by the immutable data of useState , then this is the time to use useRef.

How to think about useRef

In my opinion in React HooksuseRefat least withuseStateIs equally important, Zhihu has a sentence in an article that says,

Every developer who wants to dive into the practice of hooks must keep this conclusion in mind, not being able to use useRef comfortably will make you lose nearly half of the power of hooks.

express approval.

useCallback Hook

Regarding useCallback, the introduction on the official website is

Pass the inline callback function and the dependencies array as argumentsuseCallback, which returns a memoized version of the callback function that is only updated when a dependency changes. When you pass callbacks to optimized and use reference equality to avoid unnecessary rendering (e.g.shouldComponentUpdate), it will be very useful.

I saw the familiar word again-&quot;dependencies&quot;, in order to ensure that the function wrapped in useCallback captures the value inside the function component at the current rendering time, you must put all the values ​​inside the function component referenced in the function wrapped in useCallback into the dependencies. In addition, please pay attention to the official website The role of the introduced useCallback is – &quot;performance optimization”。

Do you really need to wrap useCallback for every function in a function component? Take the official documentationshouldComponentUpdateFor example, we define a function in the function componenthandleClickAnd wrap it with useCallback, then pass it to the child component through props, and pass it in the child componentshouldComponentUpdateComparedhandleClick, to decide whether an update is required.

function Parent () {
  const handleClick = useCallback(()=>{
    //...
  },[...])
  
  return (<Child handleClick={handleClick}/>)
}

class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    return this.props.handleClick !== nextProps.handleClick
  }
  // ...
}

Then there should be a question at this time, how big is the performance improvement. If you are interested, you might as well get started, write an example, and compare the performance performance panel, you should see how much useCallback improves performance, and according to From the test results, you can roughly get when you should use useCallback to improve performance. Another function of useCallback is to keep the function reference address unchanged. But it still regenerates the function when the dependencies change. If you want to keep the function reference address unchanged, you need to use useRef

The reason why I resist useCallback is that in my opinion, its function is relatively useless, and using useCallback, you must pay attention to dependencies, which will bring additional mental burden.

useMemo Hook

There are not many points to pay attention to when using useMemo, and the official documents are also very clear.

What useMemo does is

Pass in the &quot;create&quot; function and the array of dependencies as argumentsuseMemo, which only recalculates the memoized value when a dependency changes. This optimization helps avoid expensive computations on every render.

Please focus on &quot;expensive computation”, sometimes, useMemo may not be needed

Things to keep in mind when using useMemo

you can put**useMemo**As a means of performance optimization, but don't take it as a semantic guarantee.In the future, React may choose to &quot;forget&quot; some of the previous memoized values ​​and recalculate them on the next render, such as freeing memory for off-screen components. write first withoutuseMemocode that can also be executed without – then add in your codeuseMemo, in order to achieve the purpose of optimizing performance.

Closures in function components

Combining the above, it can be concluded that in the function component, the closure problem is mainly due to the delayed call of the function, whether it is a function wrapped by useEffect or a timer callback function or an asynchronous request callback function, theyInternally captured variablebothExist in the closure generated when the external function component is executed, then if you want to avoid the troubles caused by closures, there are two ideas

Reduce the dependence of internal functions on external variables

For example, in the above example of timer counter

setCount(count + 1)
// replace with
setCount(count => count + 1)

Replacing immutable data with mutable data

The problem of closures is rarely encountered in class components because the state and props of components are accessed through this in class components. Although this.state and this.props point to immutable data, this is internally stored The data is mutable and the reference to this does not change. So is there something similar to this in function components? Have,useRef

  • For non-function types, useRef can be used instead of useState
    In this way, even the function that is called deferred can get the latest value through ref.current, because the function that is called deferred takes the reference address of the return value of useRef. This has also been used in the example above. It should be noted that if the value stored in useRef participates in rendering, such as

    function demo () {
     const text = useRef("")
      return <>{text.current}</>
    }

In this case, updating the value stored in useRef does not cause the view to re-render. But we can synchronize the view by updating another state (useState). If you can't find a state in the component that can trigger the update when the value inside useRef changes, you can also write a custom hook to force the update

function useForceUpdate () {
  const [, forceUpdate] = useReducer(x => x + 1, 0)
  return forceUpdate
}

By encapsulating it, you can get a useState that stores variable data

function useMutableState (init) {
      const stateRef = useRef(init)
    const [, updateState] = useReducer((preState, action) => {
          stateRef.current = typeof action === 'function'
            ? action(preState)
            : action
          return stateRef.current
    }, init)

    return [stateRef, updateState]
}
  • For function types, you can also keep the function reference address unchanged through useRef, and the latest value is automatically captured inside the function

    function useStableFn(fn, deps) {
      const fnRef = useRef();
      fnRef.current = fn;
      return useCallback(() => {
     return fnRef.current();
      }, []);
    }

Epilogue

It is difficult to summarize the real perfect best practices in React Hooks, and even the official documentation and blogs only describe the mental model of React Hooks. Some of the views or examples above violate the official mental model, and I have to admit that I am a lover of useRef. But for React Hooks in practice, there is no silver bullet. What matters is whether you understand how hooks work and whether you have your own guide to avoiding them.

Reference link

  • React official documentation
  • a-complete-guide-to-useeffect
  • Zhang [email protected] React Hooks series of articles