How to memoize a high order function while using useCallback?

Viewed 885

Using React Hooks, when we want to memoize the creation of a function, we have the useCallback hook. So that we have:

const MyComponent = ({ dependancies }) => {
  const memoizedFn = useCallback(() => { 
    /* ... */ 
  }, [dependancies]);

  return <ChildComponent onClick={memoizedFn} />;
}

My question is, how do we memoize the values of a high order function in a useCallback hook such as:

const MyComponent => ({ dependancies, anArray }) => {
  const memoizedFnCreator = useCallback((id) => () => {
    /* ... */
  }, [dependancies]);

  /* 
   * How do we make sure calling "memoizedFnCreator" does not result in
   * the ChildComponent rerendering due to a new function being created?
   */
  return (
    <div>
      {anArray.map(({ id }) => (
         <ChildComponent key={id} onClick={memoizedFnCreator(id)} />
      ))}
    </div>
  );
}
3 Answers

Instead of passing a "creator-function" in the HoC you can pass down a function which takes in a id as argument and let the ChildComponent create it's own click handler

In the code example below notice that the onClick in the MyComponent no longer create a unique function, but reuses the same function across all the mapped elements, however the ChildComponent creates a unique function.

const ChildComponent = ({ itemId, onClick }) => {
    // Create a onClick handler  when calls `onClick` with the item's id
    const handleClick = useCallback(() => {
        onClick(itemId)
    }, [onClick, itemId])

    return <button onClick={handleClick}>Click me</button>
}

const MyComponent = ({ dependancies, anArray }) => {
    // Memoize a function which takes in the id and performs some action
    const handleItemClick = useCallback((id) => {
        /* ... */
    }, [dependancies]);

    return (
        <div>
            {anArray.map(({ id }) => (
                <ChildComponent key={id} itemId={id} onClick={handleItemClick} />
            ))}
        </div>
    );
}

Depending on what you are trying to achieve, the optimal solution might be entirely different, but here's one way to go about it:

const MyComponent = ({ dependencies, array }) => {
  // create a memoized callbacks cache, where we will store callbacks.
  // This cache will reset every time dependencies change.
  const callbacks = useMemo(() => ({}), [dependencies]);

  const createCallback = useCallback(
    id => {
      if (!callbacks[id]) {
        // Cache the callback here...
        callbacks[id] = () => {
          /* ... */
        };
      }
      // ...and return it.
      return callbacks[id];
    },
    [dependencies, callbacks]
  );

  return (
    <div>
      {array.map(({ id }) => (
        <ChildComponent key={id} onClick={createCallback(id)} />
      ))}
    </div>
  );
};

Since the cache resets as dependencies change, your callbacks will update accordingly.

If it were me, I could avoid higher order function as much as possible. In this case, I would have something like

const MyComponent => ({ dependancies, anArray }) => {
  const memoizedFnCreator = useCallback((id, event) => {}, [dependancies]);
  return (
    <div>
      {anArray.map(({ id }) => (
         <ChildComponent key={id} onClick={(event) => memoizedFnCreator(id,event)}/>
      ))}
    </div>
  );
}
Related