Search code examples
reactjsreact-hooks

useMemo vs useState for React hooks constants


Defining a calculated (initialized) constant using React hooks can be performed in two ways that seem functionally equivalent. I don't want to get into the use cases for this, but suffice to say that there are cases where a constant value can be derived from initial props or state that isn't expected to change (think route data, bound dispatch, etc).

First, useState

const [calculatedConstant] = useState(calculateConstantFactory);

Second, useMemo

const calculatedConstant = useMemo(calculateConstantFactory, []);

Both of these seem functionally equivalent, but without reading the source code, I'm not sure which is better in terms of performance or other considerations.

Has anyone done the leg work on this? Which would you use and why?

Also, I know some people will recoil at the assumption that state can be "considered constant". I'm not sure what to tell you there. But even without state, I may want to define a constant within a component that has no state at all, for example, creating a block of JSX that doesn't change.

I could define this outside of the component, but then it's consuming memory, even when the component in question is not instantiated anywhere in the app. To fix this, I would have to create a memoization function and then manually release the internal memoized state. That's an awful lot of hassle for something hooks give us for free.

Edit: Added examples of the approaches talked about in this discussion. https://codesandbox.io/s/cranky-platform-2b15l


Solution

  • You may rely on useMemo as a performance optimization, not as a semantic guarantee

    Meaning, semantically useMemo is not the correct approach; you are using it for the wrong reason. So even though it works as intended now, you are using it incorrectly and it could lead to unpredictable behavior in the future.

    useState is only the correct choice if you don't want your rendering to be blocked while the value is being computed.

    If the value isn't needed in the first render of the component, you could use useRef and useEffect together:

    const calculatedConstant = useRef(null);
    
    useEffect(() => {
      calculatedConstant.current = calculateConstantFactory()
    }, [])
    
    // use the value in calcaulatedConstant.current
    

    This is the same as initializing an instance field in componentDidMount. And it doesn't block your layout / paint while the factory function is being run. Performance-wise, I doubt any benchmark would show a significant difference.

    The problem is that after initializing the ref, the component won't update to reflect this value (which is the whole purpose of a ref).

    If you absolutely need the value to be used on the component's first render, you can do this:

    const calculatedConstant = useRef(null);
    
    if (!calculatedConstant.current) {
      calculatedConstant.current = calculateConstantFactory();
    }
    // use the value in calculatedConstant.current;
    

    This one will block your component from rendering before the value is set up.

    If you don't want rendering to be blocked, you need useState together with useEffect:

    const [calculated, setCalculated] = useState();
    
    useEffect(() => {
      setCalculated(calculateConstantFactory())
    }, [])
    
    // use the value in calculated
    

    Basically if you need the component to re-render itself: use state. If that's not necessary, use a ref.