Since the advent of React Hooks, we have been discussing it one after another. Today, let's talk about the API React.useCallback. Let's start with the conclusion: in almost all scenarios, we have a better way to replace useCallback.
Let's first look at the usage of useCallback
const memoizedFn = React.useCallback(() => { doSomething(a, b); }, [a, b]);
React officials regard this API as a performance optimization means of React.memo. Look at the introduction:
Pass the inline callback function and dependency array as parameters into useCallback, which will return the memoized version of the callback function, which will be updated only when a dependency changes. It is useful when you pass callback functions to sub components that are optimized and use reference equality to avoid unnecessary rendering (such as shouldComponentUpdate).
Let's look at useCallback from the perspective of performance optimization.
Example:
const ChildComponent = React.memo(() => { // ... return <div>Child</div>; }); function DemoComponent() { function handleClick() { // Business logic } return <ChildComponent onClick={handleClick} />; }
The handleClick function is recreated when the DemoComponent component itself or the parent component triggers render.
Each time you render, a new onClick parameter will be accepted in the ChildComponent parameter, which will directly break through React.memo, resulting in failure of performance optimization and linked with render.
Of course, the official documentation points out that the cost of re creating functions each time render is followed within the component is almost negligible. If the function is not passed to the self component, there is no problem at all and the overhead is less.
Next, we wrap it with useCallback:
// ... function DemoComponent() { const handleClick = React.useCallback(() => { // Business logic }, []); return <ChildComponent onClick={handleClick} />; }
In this way, handleClick is the memoized version. If the dependency remains unchanged, the function created for the first time will always be returned. But every time render creates a new function, it's just not used.
React.memo is similar to PureComponent. They will shallow compare the old and new data of the incoming component. If they are the same, rendering will not be triggered.
Next, we add dependencies to useCallback:
function DemoComponent() { const [count, setCount] = React.useState(0); const handleClick = React.useCallback(() => { // Business logic doSomething(count); }, [count]); // Other logical operations setState return <ChildComponent onClick={handleClick} />; }
We define the count state as the dependency of useCallback. If the count changes, render will generate a new function. This will break down React.memo and link the sub component render.
const handleClick = React.useCallback(() => { // Business logic doSomething(count); }, []);
If the dependency is removed, the value of count obtained by the internal logic is always the initial value, that is, 0, that is, the latest value cannot be obtained. If the internal logic is extracted as a function as a dependency, it will lead to the invalidation of useCallback.
Let's look at the useCallback source code
ReactFiberHooks.new.js
// Loading phase function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T { // Get the corresponding hook node const hook = mountWorkInProgressHook(); // If the dependency is undefiend, it is set to null const nextDeps = deps === undefined ? null : deps; // Staging current functions and dependencies hook.memoizedState = [callback, nextDeps]; return callback; } // Update phase function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // Get the last staged callback and dependency const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; // Make a shallow comparison between the last dependency and the current dependency, and return the last temporary function if it is the same if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } // Otherwise, the latest function is returned hook.memoizedState = [callback, nextDeps]; return callback; }
Through the source code, it is not difficult to find that the implementation of useCallback is through the function defined by the temporary storage, whether to update the temporary storage function according to the before and after dependency comparison, and finally return this function, so as to generate closures to achieve the purpose of memory.
This directly leads me to want to use useCallback to get the latest state, so I must add this state to the dependency to generate a new function.
As we all know, ordinary function s can promote variables, so that they can call each other without paying attention to the writing order. If it is implemented by useCallback, in the era when eslint disabled var, the first declared useCallback cannot be called directly, let alone recursive.
Component unloading logic:
const handleClick = React.useCallback(() => { // Business logic doSomething(count); }, [count]); React.useEffect(() => { return () => { handleClick(); }; }, []);
When a component is unloaded, if you want to call to get the latest value, can't you get the latest status? In fact, this can't be regarded as the pit of useCallback. This is the design of React.
Well, we've listed some questions whether it's useCallback or not.
- The memory effect is poor, and the dependency value is recreated when it changes
- If you want a good memory effect and a closure, you can't get the latest value
- Context call order problem
- Getting the latest state during component uninstallation
I want to avoid these problems, okay? Bring it, you!
Let's look at the usage first
function DemoComponent() { const [count, setCount] = React.useState(0); const { method1, method2, method3 } = useMethods({ method1() { doSomething(count); }, method2() { // Call method1 directly this.method1(); // Other logic }, method3() { setCount(3); // more... }, }); React.useEffect(() => { return () => { method1(); }; }, []); return <ChildComponent onClick={method1} />; }
Is the usage simple? There is no need to write dependencies, which not only perfectly avoids all the above problems. It also makes our function aggregation easy to read. No more nonsense, on the source code:
export default function useMethods<T extends Record<string, (...args: any[]) => any>>(methods: T) { const { current } = React.useRef({ methods, func: undefined as T | undefined, }); current.methods = methods; // Initialize only once if (!current.func) { const func = Object.create(null); Object.keys(methods).forEach((key) => { // Package function forwarding calls the latest methods func[key] = (...args: unknown[]) => current.methods[key].call(current.methods, ...args); }); // Variable returned to user current.func = func; } return current.func as T; }
The implementation is very simple. useRef is used to temporarily store object s. During initialization, each value is wrapped with a function for forwarding and obtaining the latest function. Thus, we can not only get the latest value, but also ensure that the reference value will never change during the declaration cycle.
Perfect, that's it ~
So is there no use scenario for useCallback? The answer is No. in some scenarios, we need to temporarily store the value of the closure of a state through useCallback for demand. For example, the message pop-up box needs to pop up the status information temporarily stored at that time, rather than the latest information.
Finally, I recommend the state management I wrote Heo , useMethods has been included. Thank you for clicking star. Later, you will share the motivation of writing heo library. Welcome to WeChat's official account.