DEV Community

Vladimir Klepov
Vladimir Klepov

Posted on • Originally published at blog.thoughtspile.tech on

How useRef turned out to be useMemo's father

It’s no secret that react’s useCallback is just sugar on top of useMemo that saves the children from having to see an arrow chain. As the docs go:

useCallback((e) => onChange(id, e.target.value), [onChange, id]);
// is equivalent to
useMemo(() => (e) => onChange(id, e.target.value), [onChange, id]);
Enter fullscreen mode Exit fullscreen mode

A less known, probably useless, but very fun, fact: you can actually pass something other than a function to useCallback and have it memoized.

const stableValue = useCallback({ please: 'dont do this' }, []);
// yes-yes, stableValue is that object, as of react@17.0.2

As I got more into hooks, I’ve been surprised to realize how similar useMemo itself is to useRef. Think about it that way: useRef does a very simple thing — persists a value between render function calls and lets you update it as you wish. useMemo just provides some automation on top for updating this value when needed. Recreating useMemo is fairly straightforward:

const memoRef = useRef();
const lastDeps = useRef(deps);
// some shallow array comparator, beside the point
if (!arrayEquals(deps, lastDeps.current)) {
    memoRef.current = factory();
    lastDeps.current = deps;
}
const memoized = memoRef.current;
// ... is equivalent to const memoized = useMemo(factory, deps);
Enter fullscreen mode Exit fullscreen mode

As a special case, raw useRef is almost the same as useMemo with no deps, save for actually building the initial value on every render and then throwing it away:

const stableData = useRef({}).current; // same as useMemo(() => {}, []);
Enter fullscreen mode Exit fullscreen mode

Treating useRef as a stripped-down useMemo can prove useful in some cases. If the built-in caching mechanism does not work for you, useRef is a perfect way to tweak it. Some motivational examples:

  • Actually cache all the previous results using eg fast-memoize. useMemo appears to just cache the last result, which is a good default.
  • Support true array dependencies with dynamic length: useArrayMemo(() => hash(arrayValues), arrayValues)
  • Use an object instead of an array: useObjectMemo(() => props, props) gives you the same reference unless a prop has changed.
  • More generally, allow any custom comparator for deps: useCustomMemo(() => lib.sum(table1, table2), [table1, table2], (a, b) => a.equals(b))

These may not be the most common use cases, but it’s good to know that this is doable, and that useRef is there to help you in case you ever need it.

On to another fun fact — you can make useMemo return a constant-reference RefObject box, equivalent to useRef. It’s not clear why you would want that.

useMemo(() => ({ current: initialValue }), [])

So, wrapping up:

  1. useCallback is just tiny sugar on top of useMemo.
  2. useMemo is just useRef with auto-update functionality.
  3. You can build customized versions of useMemo with useRef.
  4. You can bend useCallback to be a useMemo, and you can get useMemo to be a useRef, but that doesn’t mean you should.

On the other hand, useState (and useReducer) is an entirely different cup of tea, since they can trigger a rerender on update. More on these guys in the next post!

Top comments (0)