Hooks

Hooks are a powerful tool for sharing stateful logic across components, but have some odd non-intuitive properties

Things to know about hooks

Object Identity

The first thing to know about hooks (and React in general) is they only care about object identity when doing change detection. So 1 === 1, and changing a value from 1 to 1 won't trigger a change. But {foo: 'bar'} !== {foo: 'bar'}, because the object references are different, even if the contents happen to be the same. So something like

const foo = 'bar';
const x = { foo };
useEffect(() => console.log(x), [x]);

will (perhaps unexpectedly) log on every render loop, because every time React renders, it will run the component function body, create a new object reference in x, and re-run the useEffect - even though the data (bar) is the same.

To prevent this, you can use useMemo to only re-create the object reference only when its underlying data changes, like so

const foo = 'bar';
const x = useMemo(() => ({ foo }), [foo]);
useEffect(() => console.log(x), [x]);

The useEffect will now only trigger if foo changes.

This is also the main use case for useCallback - if you are defining a function in a React component body, and calling that function inside a useEffect, that function must be wrapped in a useCallback (or extracted outside of the component), or on every render React will see a few object reference for the function and re-run the useEffect. Example:

const foo = 'bar';
const log = useCallback(() => console.log(foo), [foo]);
useEffect(() => log(x), [x, log]);

Types of hooks

Because change detection is so important, there are three primary types of hooks:

Holds state and triggers re-renders: useState/useReducer/zustand

These hooks hold some state, and when updated, will trigger a React render loop for all child components

Performs work on change: useEffect

This doesn't hold state, but simply runs when some state changes

Reduces changes: useRef/useMemo/useCallback

These hooks all exist to help you work around React's change detection mechanism. useMemo/useCallback both directly skip object changes, and useRef is for using mutation to never change object references.

When to not useMemo/useState

Once you start using useMemo/useCallback, it can be tempting to use it everywhere - after all, if it skips rendering, why not wrap every calculation in a useMemo? However, this is usually a mistake if done purely for performance. A simple calculation that returns a primitive (like a number or string, which don't trigger change detection if its value doesn't change between renders) is almost always less work than setting up a new function to potentially run and then checking a cache to decide whether or not to run it. So as a rule of thumb, unless you need to prevent a useEffect, just do the calculation on every render. And if you find performance problems on a page, use the React performance profiler before adding useCallback/useMemo.

// worst: two hooks for simple addition
const Adder = ({ a, b }) => {
  const [value, setValue] = useState(0);
  useEffect(() => {
    setValue(a + b);
  }, [a, b]);
  
  return <div>{value}</div>
}
// still bad: unnessecary useMemo
const Adder = ({ a, b }) => {
  // Creating the `() => a + b` function, caching a and b,
  // and checking them against the incoming values
  // is way more work than just a + b
  const value = useMemo(() => a + b, [a, b]);
  
  return <div>{value}</div>
}
// best: just do the math
const Adder = ({ a, b }) => {
  const value = a + b;
  
  return <div>{value}</div>
}

useState to memoize

If you have a value that should be calculated once, and never again even if other state changes, then instead of useMemo you can use useState

Note that often you can just extract the value into a constant out of the component, but this technique is useful if the value should be unique per component

const [myMemoizedValue] = useState(() => calcualteMemoziedValue());
// or
const myMemoizedValue = calcualteMemoziedValue();
const MyComponent = () =>{
}

Last updated