In our last article, we talked about some of the changes in the React development experience brought about by Hooks. If you’ve already started trying out React Hooks, you may have encountered one as confusing as I did, but if not, that’s great, I’ll keep it as a record for others to use.

How do I bind events?

Let’s start with the official example:

import React, { useState } from 'react';

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

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

See that anonymous function with the onClick binding? Written this way, a new function is regenerated each time render is performed. This might not have been too much of a concern before, because we generally implemented presentation components with Function Components and didn’t have too many children under them. But if we embrace Hooks, then it’s out of control.

In general, this doesn’t cause much of a performance problem, and the Function Component itself performs better than the Class Component, but there will be times when you need to optimize. For example, when refactoring the original Class Component, one of the child components is a PureComponent, which invalidates the optimization of the child Component.

useuseCallbackoruseMemoTo save a reference to a function and avoid generating new functions repeatedly

function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback((a)= > {
    setCount(count= > count + 1)}, []);// Use useMemo
  // const handleClick = useMemo(() => () => {setCount(count => count + 1)}, []);
   
  return (
    <div>
      <p>count: {count}</p>{/* Child = PureComponent */}<Child callback={handleClick} />
    </div>)}Copy the code

Inputs useCallback(FN, inputs) equals useMemo(() => FN, inputs). We can get a glimpse of this in the source code, using useCallback as an example (useMemo is basically the same, but returns different values, as discussed later).

React calls mountCallback from ReactFiberHooks the first time useCallback is executed, and the next time it is executed it calls updateCallback. Github.com/facebook/re…

Let’s start with mountCallback:

function mountCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
Copy the code

The core of the discovery lies in the method of mountWork in Progress Shook

function mountWorkInProgressHook() :Hook {
  const hook: Hook = {
    memoizedState: null.baseState: null.queue: null.baseUpdate: null.next: null};if (workInProgressHook === null) {
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
Copy the code

From the following code we know the ontology of the Hooks:

const hook = {
  memoizedState: null.baseState: null.queue: null.baseUpdate: null.next: null,}Copy the code

MemoizedState and memoizedState will be stored in different hooks. MemoizedState is stored in useCallback as the input parameter [callback, deps], next as the next Hook. That is, Hooks are a one-way list, which explains why Hooks need to be called at the top level, not in loops, conditional statements, nested functions, because they need to be called in the same order.

Let’s look at updateCallback:

function updateCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  // This hook is the first hook to be mounted
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // memoizedState is the memoizedState of mount [callback, deps]
  const prevState = hook.memoizedState;
  if(prevState ! = =null) {
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // Compare the deps twice and return the previously stored callback instead of the newly passed callback
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
Copy the code

The implementation of useMemo is similar to that of useCallback.

function mountMemo<T> (nextCreate: () = >T.deps: Array<mixed> | void | null) :T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // Unlike useCallback, memoizedState stores the results of the implementation of nextCreate
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
    
  // Returns the execution result
  return nextValue;
}

function updateMemo<T> (nextCreate: () = >T.deps: Array<mixed> | void | null) :T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if(prevState ! = =null) {
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0]; }}}// The same is true for nextCreate
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  
    // Returns the execution result
  return nextValue;
}
Copy the code

The above code shows the difference between useCallback and useMemo.

In addition to these two methods, you can also pass the Dispatch method generated by the useReducer through the context to avoid passing the callback directly, since the dispatch is immutable. This approach is fundamentally different from the previous two in that it prevents the callback from being passed at the source, so there are no performance concerns mentioned earlier, and it is officially recommended, especially if the component tree is large. So the above code, if written this way, would look like this, somewhat like Redux:

import React, { useReducer, useContext } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    default:
      throw new Error();
  }
}

const TodosDispatch = React.createContext(null);

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

  return (
    <div>
      <p>count: {state.count}</p>
      <TodosDispatch.Provider value={dispatch}>
        <Child />
      </TodosDispatch.Provider>
    </div>
  )
}

function Child() {
  const dispatch = useContext(TodosDispatch);
  return (
    <button onClick={()= > dispatch({type: 'increment'})}>
      click
    </button>)}Copy the code

conclusion

  • In general, event bindings can be handled directly by arrow functions with no obvious performance issues and are easy to write.
  • If required, you can passuseCallbackoruseMemoTo optimize.
  • If the component tree is large, passcallbackThe hierarchy may be deep enough to passuseReducerCooperate withcontextTo deal with.

The above is just some of my personal ideas, if there is any wrong place, welcome to correct ~ ~

React Hooks