React.memo vs useMemo for private components

Viewed 193

Imagine you have a component expecting a function that renders a specific subtree in that component's hierarchy:

const Timeline = ({ renderDetails, ... }: { renderDetails: (rowData, sectionID, rowID) => React.Node }) => <div>{renderChildren(...)}</div>;

Is there a difference in using:

const MyComponent = React.memo(({ someProp }) => <someComponentHierarchyHere>...</someComponentHierarchyHere>);

const ParentComponent = ({ someProp }) => {
    const renderChildren = useCallback(() => <MyComponent someProp={someProp} />, [someProp]);

    return <Timeline renderChildren={renderChildren} />;
};

vs

const ParentComponent = ({ someProp }) => {
    const MyComponent = useMemo(({ someProp }) => <someComponentHierarchyHere>...</someComponentHierarchyHere>);
    const renderChildren = useCallback((rowData, sectionID, rowID) => <MyComponent someProp={someProp} />, [someProp]);

    return <Timeline renderChildren={renderChildren} />;
};

or simply:

const ParentComponent = ({ someProp }) => {
    const renderChildren = useCallback((rowData, sectionID, rowID) => <someComponentHierarchyHere>...</someComponentHierarchyHere>, [someProp]);

    return <Timeline renderChildren={renderChildren} />;
};

? I'm specifically interested in the decisions React renderer will take, as well as whether the MyComponent component (or the hierarchy it inserts in the component tree) will be memoized.

2 Answers

In general, component definitions should always be static, so React.memo is the correct option here.

If your inner component is completely stateless, useMemo will appear to work, but you won't be able to use any hooks inside of the inner component, so for anything complex, you would have to do it the other way anyhow.

The reason for this is that, internally, React keeps track of what component is rendering at any given time, and that's how it determines what values are returned by useState and other hooks. Note that you're not using a dependencies array in your useMemo, meaning that it will be recreated on every render, so it's effectively pointless (the react hooks linting rules will warn you about this. However, even with an empty dependencies array, it still would not be safe to use hooks in it. A common misconception is that is useMemo with no dependencies will create a constant, but this is not the case. It's an optimization that usually prevents recalculation of values, but it's not guaranteed. Internally, React can choose to discard the memoized result of useMemo at any time. If that happened, it would randomly discard your state, so React prohibits it entirely.

import { useMemo, useState } from 'react';

export default function App() {
  const MyComponent = useMemo(() => {
    // This line errors:
    // React Hook "useState" cannot be called inside a callback.
    // React Hooks must be called in a React function component or a
    // custom React Hook function. (react-hooks/rules-of-hooks) eslint
    const [input, setInput] = useState('')
  
    return (
      <input value={input} onChange={(e) => setInput(e.target.text)} />
    )
  })

  return <MyComponent />;
}

Both have a similar name and are optimization methods. But that's where the similarity ends, they're totally distinct React features.

React.memo is for when you find an existing component that is expensive to render, and you don't have the option to optimize it internally. Just wrap it and let React do an extra check.

React.useMemo is for internally optimizing a component by saving the return value of an expensive function call.

Implementation

useMemo

The source code is relatively straightforward. The previous value is returned if the deps didn't change.

const prevDeps: Array<mixed> | null = prevState[1];
if (prevState !== null) {
    if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
    }
}

const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;

In all other cases it calls the (supposedly expensive) nextCreate function. The result is first saved in the hook log (for use in next renders) and then returned.

For the initial ("mount") render, React uses a simpler implementation that just calls nextCreate, saves it, and returns it.

React.memo

React.memo is quite a different type of function. It's a sort of Higher Order Component, but with a catch. It doesn't return a regular React component, but a special REACT_MEMO_TYPE. This type ultimately results in special handling of these elements while executing render work.

There's more moving parts to this but for the sake of simplicity I just copied the props check.

let compare = Component.compare; // This is what you pass into 2nd argument.
compare = compare !== null ? compare : shallowEqual;

if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

Unlike useMemo it doesn't involve storing any additional state. Instead the comparison function is used to avoid calling the (supposedly expensive) render function. It simply aborts a chunk of work before starting it, when it sees it would be useless.

Conclusion

These are really 2 distinct things with a similar name and purpose.

Applicability to OP use case

You'll probably want React.memo.

Of course you always first measure if there's anything to optimize at all. Each of these things has a cost on its own. You can't sprinkle it around and automatically gain performance. If it's not balanced by avoiding expensive tasks, it's only making the code slower and more complex.

The following doesn't do anything useful:

useMemo(({ someProp }) => <someComponentHierarchy>...</someComponentHierarchy>)

If you check the React source above, you see the nextCreate function is called without arguments.

Regardless of what you put in it, useMemo will not result in the same check for the Memoized element type as React.memo does, so it can't be used for the same purpose.

Related