If you don’t know what hooks are, read what the official documentation says first.

I’d like to start with useCallback, because it doesn’t affect our code logic, and is intended for those of you who are performance-oriented or obsessive-compulsive. This seemingly simple hook actually hides a lot of interesting things.

Super many functions created and useCallback

At first glance the React code using Hooks might wonder if creating so many inline functions would affect performance. Didn’t React always advise against creating new functions in callback? First, take a look at the official explanation, which states that the performance of closure functions in JavaScript is very fast and not much worse thanks to lighter function components than class and the avoidance of additional layers of HOC, renderProps, and so on. React provides useMemo and useCallback(FN, inputs) === useMemo(() => FN, inputs)). Some people might mistakenly think that useCallback can be used to solve the performance problems caused by creating functions. In fact, useCallback is only slower from this component alone because inline functions are created anyway, Added internal check for inputs change in useCallback.

function A() { // ... Const cb = () => {}/* creates */; } function B() { // ... Const cb = React. UseCallback (() = > {} or created * / / * and [a, b]); }Copy the code

The real purpose of useCallback is to cache an instance of the inline callback for each rendering, so that it can be used with shouldComponentUpdate or react.Memo to reduce unnecessary rendering. Note that in a future where most callback will be inline, the react. memo and the react. useCallback need to be used together. After all, meaningless shallow comparisons cost a little bit of performance. Let’s digress a little bit. In fact, not only Hooks and functional components, but also class-based components sometimes have this problem. In many list renderings, it is inevitable to write an arrow function:

class SomeComponent extends React.PureComponent { render() { const { list, thingsNeedToUseInCallbackButDoNotNeedInChild, onChange } = this.props; return ( <ul> {list.map(item => <Item key={item.key} onClick={() => { onChange(item, thingsNeedToUseInCallbackButDoNotNeedInChild) }} /> )} </ul> ); }}Copy the code

Because of the PureComponent preference, this Item also inherits the React PureComponent. But since onClick uses inline functions, the PureComponent’s default shallow comparison is also meaningless.

We can also create a custom memoize for callback following the useCallback theme:

Memoize is used to cache JavaScript function results and speed up code

import { memoize } from 'decko'; class SomeComponent extends React.PureComponent { @memoize getItemChangeHandler = (key, item) => () => { const { thingsNeedToUseInCallbackButDoNotNeedInChild, onChange } = this.props; onChange(item, thingsNeedToUseInCallbackButDoNotNeedInChild); }; render() { const { list } = this.props; return ( <ul> {list.map(item => <Item key={item.key} onClick={this.getItemChangeHandler(item.key, item)} /> )} </ul> ); }}Copy the code

To sum up, getting back to Hooks, the purpose of useCallback is to reduce invalid reRender with Memoize for performance optimization purposes. It’s the same old mantra, “Don’t optimize performance too early.” As a rule of thumb, it is important to observe and compare the results of these types of optimizations, because a callback in a small corner can cause the optimization to fail or even backfire.

Does useCallback work for all scenarios?

Check the inputs for useCallback: they are mistaken for the first parameter: inputs: One complication is that, under the current implementation, references to a callback cannot be cached if they depend on a frequently changing state. The React FAQ also addresses this problem.

function Form() { const [text, updateText] = useState(''); const handleSubmit = useCallback(() => { console.log(text); }, [text]); Return (<> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onSubmit={handleSubmit} /> }Copy the code

The reason this problem is unsolvable is that the access to state inside the callback depends on the closure of the JavaScript function. If callback is expected to remain unchanged, state in the closure of the callback function before it is accessed will always be the same value at that time. Let’s take a look at the React document:

function Form() { const [text, updateText] = useState(''); const textRef = useRef(); useLayoutEffect(() => { textRef.current = text; // write text to ref}); const handleSubmit = useCallback(() => { const currentText = textRef.current; // Read text Alert (currentText) from ref; }, [textRef]); // handleSubmit only relies on textRef changes. Return (<> <input value={text} onChange={e => updateText(e.target.value)} /> <ExpensiveTree onSubmit={handleSubmit} /> </> ); }Copy the code

The solution presented in the documentation may not be easy to understand at first glance, so let’s take it one step at a time. First, since functional components don’t use this to store instance variables, React recommends using useRef to store variable values that change. UseRef is no longer just for DOM refs, but also for component instance attributes. After updateText completes the text update, write textref.current to useLayoutEffect (equivalent to didMount and didUpdate). This way, the value stored in the textRef retrieved from handleSubmit is always the new value.

Did you feel like you had an Epiphany. Essentially what we want to achieve is the following:

  1. A callback that takes full advantage of the same functionality generated when a functional component is rendered multiple times
  2. The callback can access the latest state inside the functional component, regardless of closures

Because functional components restrict access to component instances. The method above uses useRef to create a ref that generally does not change over multiple render sessions, and then updates the values needed to access the ref to “penetrate” the closure. So is there another way?

function useCallback(callback) { const callbackHolder = useRef(); useLayoutEffect(() => { callbackHolder.current = fn; }); return useMemo( () => (... args) => (0, ref.current)(... args), [] ); }Copy the code

This is a different version from the current useCallback implementation inside React (see from issue). Conversely, create a ref to hold the latest callback and return an immutable “springboard” function to actually call the latest function. This has the added advantage of not relying on explicit inputs declarations.

Is that perfect? Definitely not. Otherwise it must have been an official implementation. At first glance this function doesn’t seem to introduce any problems, but if you look closely, updating the ref.current during DOM updates will result in this function not being called during the render phase. Worse, because of the ref change, there may be some weird situations in the future React asynchronous mode (hence the official solution above).

It is to be expected that the community is actively discussing these issues and solutions encountered by useCallback. The React team also planned a more complex, but effective, version of React internally.

Now what?

For all the reasons mentioned above, the best solution is to use useReducer. Because the reducer is actually executed at the next render, the reducer is always accessed with the new props and state.

const TodosDispatch = React.createContext(null); Function TodosApp() {// Tip: 'dispatch' does not change const [todos, dispatch] = useReducer(todosReducer); return ( <TodosDispatch.Provider value={dispatch}> <DeepTree todos={todos} /> </TodosDispatch.Provider> ); }Copy the code

The dispatch function returned by useReducer comes with Memoize and does not change over multiple renders. So if you want to pass state as a context, declare it in two contexts.

conclusion

When we look deeply into the use and implementation of useCallback, do we feel that the seemingly simple API also contains a lot of mystery? There are a lot of small details that Hooks fail to find best practices in use, and it would be interesting to follow developers as they continue to explore them. React issues