banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: useCallback & useMemo

Explanation#

  1. 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.
  2. To read this article, you need to read the React Hooks: hooks chain first.
  3. Like most other hooks, each hook is explained in terms of the mount and update phases.
  4. 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.

image

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.

image

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.

image

/**
 * 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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.