preface

AHooks is an alibaba open source React Hooks library, many of which are implemented smarty. UsePersistFn is the focus of this article.

Here is the usePersistFn document

usePersistFn

What problem does usePersistFn solve?

In some scenarios, you might need to remember a callback using useCallback, but since the internal function must be recreated frequently, the memory is not very good, causing the child component to repeat render. For super complex subcomponents, rerendering can have an impact on performance. With usePersistFn, the function address is guaranteed to never change.

Write a specific Demo to describe the above scenario

function Child(props) {
  console.log("child render");
  return <button onClick={props.showCount}>showCount</button>;
}

const ChildMemo = memo(Child);

function App() {
  const [count, setCount] = useState(0);

  const showCount = useCallback(() = > {
    console.log("showCount"); } []);return (
    <div className="App">
      <button onClick={()= >SetCount ((val) => val + 1)}> triggers parent component rendering</button>
      <h2>count:{count}</h2>
      <ChildMemo showCount={showCount} />
    </div>
  );
}
Copy the code

To reduce rendering of the Child component, we use memo with useCallback, and useCallback’s second argument passes an empty array, so showCount is created only once, so that when the parent component rerenders, ChildMemo is not rerendered. The goal of reducing the number of unnecessary renders is achieved.

But when we want to access the count variable in showCount

const showCount = useCallback(() = >{+console.log(count); } []);Copy the code

The value of count is not updated. This is a result of the React-hooks implementation, which can be described as a closure trap. Add count to the dependency.

const showCount = useCallback(() = >{+console.log(count);
  }, [count]);
Copy the code

The problem is resolved and the latest count can be accessed, but each time the count changes, showCount is recreated to produce a new function, invalidating the memo.

React has a temporary solution to this problem: use useRef.

The react. HTML. Cn/docs/hooks -…

function Child(props) {
  console.log("child render");
  return <button onClick={props.showCount}>showCount</button>;
}

const ChildMemo = memo(Child);

function App() {
  const [count, setCount] = useState(0);
  const countRef = useRef();

  useEffect(() = > {
    countRef.current = count;
  }, [count]);

  const showCount = useCallback(() = > {
    console.log(countRef.current);
  }, [countRef]);

  return (
    <div className="App">
      <button onClick={()= >SetCount ((val) => val + 1)}> triggers parent component rendering</button>

      <h2>count:{count}</h2>

      <ChildMemo showCount={showCount} />
    </div>
  );
}
Copy the code

We could use custom hooks, which is how React officially uses useRef

function usePersistFn(fn, deps) {
  const fnRef = useRef();

  useEffect(() = > {
    fnRef.current = fn;
  }, [fn, ...deps]);

  return useCallback(() = > {
    return fnRef.current();
  }, [fnRef]);
}
Copy the code

Such use

function App() {
  const [count, setCount] = useState(0);

  const showCountWithPersist = usePersistFn(() = > {
    console.log(count);
  }, [count]);

  return (
    <div className="App">
      <button onClick={()= >SetCount ((val) => val + 1)}> triggers parent component rendering</button>

      <h2>count:{count}</h2>

      <ChildMemo showCount={showCountWithPersist} />
    </div>
  );
}
Copy the code

Codesandbox.io /s/rough-daw…

However, every time we use it, we need to pass the dependency, which is troublesome. We can optimize it so that we do not need to pass the dependency.

The root reason we pass in the dependency is because we want the dependency to change and need to reassign fn to fnref.current

useEffect(() = > {
    fnRef.current = fn;
  }, [fn, ...deps]);
Copy the code

As long as we don’t check whether the dependency changes, we reassign fn to fnref.current every time the function executes, regardless of whether the dependency changes or not, then we don’t need the user to pass the dependency

function usePersistFn(fn) {
  const fnRef = useRef();
+  fnRef.current = fn; UseEffect is removed from this line

  return useCallback(() = > {
    return fnRef.current();
  }, [fnRef]);
}
Copy the code

This section of code codesandbox. IO/s/unruffled…

Ahooks usePersistFn (github.com/alibaba/hoo…) This is the way to do it without passing dependencies.

The only difference is that it replaces useCallback with useRef, but the end result is the same: the returned reference is guaranteed to be the same when usePersistFn is called multiple times.

  • Ahooks (ahooks.js.org/zh-CN/hooks…).
function usePersistFn(fn) {
  const fnRef = useRef(fn);
  fnRef.current = fn;

  const persistFn = useRef();
  if(! persistFn.current) { persistFn.current =function (. args) {
      return fnRef.current.apply(this, args);
    };
  }

  return persistFn.crrent;
}
Copy the code

The ref to persistFn is first created and then rendered for the first time. Persistfn.current returns true, and the anonymous function is assigned to persistfn.current.

Return fnref.current.apply (this, args); Use apply only to ensure that this points. Anonymous functions can also be replaced with arrow functions, so there is no need to apply, for example:

 persistFn.current =  (. args) = > fnRef.current(args);
Copy the code

fromReactOfficial advice

We recommend passing dispatches in the context rather than calling callbacks separately in props(properties). For completeness and as an escape hatch, only the following methods are mentioned here. Also note that this pattern can cause problems in concurrent mode. We plan to provide more customizable alternatives in the future, but the safest solution for now is to always invalidate callbacks if a value depends on change.

The main points are as follows

  1. When faced with the need for propsThe child/sunThis is used when components are passed layer by layerContextCooperate withdispatch

The problem is that there’s a lot of garbage rendering going on right now when you use Context directly, and there’s a lot of things you need to be aware of in order to reduce garbage rendering, like the following

  • Break up Context with different granularity

    const App = () = > {
      // ...
      return (
        <ContextA.Provider value={valueA}>
          <ContextB.Provider value={valueB}>
            <ContextC.Provider value={valueC}>.</ContextC.Provider>
          </ContextB.Provider>
        </ContextA.Provider>
      );
    };
    Copy the code
  • Pay attention to the order of the Context, keep the invariant in the outer layer, the variable in the inner layer.

conclusion

  • Use cautionuseRefIn order to achieveClosure throughThe effect inReact18Concurrency mode (Concurrent Mode) Unexpected results may occur.
  • Not recommendedContextIf not, use it with care to avoid additional rendering behavior. The following principles should be maintained
    1. Break upContext
    2. Focus onContextThe order, let the constant in the outer layer, the variable in the inner layer.
    3. In the currentReact ContextThe lack ofcontext selectorsIn the case of this mechanism, it is recommended to use a state management library instead of the Context, after all, most state management libraries have itselectorsMechanisms to optimize performance.

Reference

  • zhuanlan.zhihu.com/p/313983390
  • The react. HTML. Cn/docs/hooks -…