Explanation#
- useCallback and useMemo are optimization hooks that can help reduce unnecessary rendering and computation in React components. However, this article only analyzes the implementation of these two hooks and does not explain how to use them. The version used in this article is based on v18.1.0.
- To read this article, you need to read the React Hooks: hooks chain first.
- Like most other hooks, each hook is explained in terms of the mount and update phases.
- The analysis is based on the following code:
import { useCallback, useEffect, useMemo, useState } from "react";
function UseCallbackAndUseMemo() {
const [a, setA] = useState('a');
const [b] = useState('b');
const memoizedCallback = useCallback(
() => {
console.log(a, b);
},
[a, b],
);
const memoizedResult = useMemo(() => {
return {
a,
b
}
}, [a, b]);
useEffect(() => {
setTimeout(() => {
setA('aaa');
}, 3000);
}, []);
useEffect(() => {
console.log(memoizedResult, 'memoizedResult change');
}, [
memoizedResult
]);
return (
<div onClick={ memoizedCallback }>click here</div>
)
}
export default UseCallbackAndUseMemo;
useCallback#
useCallback in the mount phase#
When a breakpoint is set on useCallback, it enters the mountCallback function.
Let's see what's inside.
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// Create a hook object, see the analysis in the hooks chain
const hook = mountWorkInProgressHook();
// Assign the dependencies
const nextDeps = deps === undefined ? null : deps;
// Store the function and dependencies on the memoizedState of the hook object
hook.memoizedState = [callback, nextDeps];
// Return the function
return callback;
}
That's it. Now let's see what happens in the update phase.
useCallback in the update phase#
Keep the breakpoint unchanged, wait for the timer to execute, and then enter the updateCallback function.
Let's see what's inside.
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// Move the pointer of the hooks chain to the hook object corresponding to the current useCallback
const hook = updateWorkInProgressHook();
// Get the dependencies passed in the update phase
const nextDeps = deps === undefined ? null : deps;
// Get the [callback, deps] stored on the hook object in the previous time
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
/**
* When prevState is not null (which should always be the case...)
* and nextDeps is not null, enter this condition
*/
// Get the dependencies (deps) inside memoizedState
const prevDeps: Array<mixed> | null = prevState[1];
// Compare whether the previous and current dependencies are the same
if (areHookInputsEqual(nextDeps, prevDeps)) {
// If they are the same, return the previously cached function
return prevState[0];
}
}
}
// If the above conditions are not met, update the data inside memoizedState
hook.memoizedState = [callback, nextDeps];
// Return the function
return callback;
}
Basically, it determines whether the previous cached function can be reused by comparing the previous and current dependencies. It's a similar implementation to closures. To explain it with an example, consider the following code:
function exampleA(a) {
let number = a;
return function example() {
return a + 2
}
}
function exampleB(b) {
let number = b;
return function example() {
return b + 2
}
}
const func1 = exampleA(2);
const func2 = exampleB(2);
When the same value is passed to both functions, although both functions will return a result of 4, func1 and func2 are definitely not the same function. If passed into a React component, it will inevitably cause the props to change and trigger a re-render of the component, but even after the re-render, the page will not change. So, to prevent unnecessary re-rendering, a cached function with the same execution result is used.
Next, let's see how areHookInputsEqual compares the previous and current dependencies.
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
// Traverse both arrays at the same time and compare them one by one
// is is a shallow comparison using ===
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
Summary of useCallback#
By storing the function and dependencies on the corresponding hook object, useCallback compares the previous and current dependencies during the update phase to determine whether the function can be reused, thus achieving function reuse and avoiding unnecessary updates of the component.
useMemo#
useMemo in the mount phase#
When a breakpoint is set on useMemo, it enters the mountMemo function.
/**
* Almost the same logic as the mount phase of useCallback
*/
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// The only difference is here, useMemo stores the result of the function execution
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
useMemo in the update phase#
Keep the breakpoint unchanged, wait for the timer to execute, and then enter the updateMemo function.
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) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
It's exactly the same logic as the update phase of useCallback. That's it.
Summary of useMemo#
The implementation of useMemo is almost identical to useCallback, with the only difference being that useMemo stores the result of the function execution, while useCallback stores the function itself.