React hooks
Last modified on Thu 02 Jun 2022

React Hooks were introduced in version 16.8.0 as function component counterpart of class component lifecycles. For more information, see the official React Hooks API Reference.

Hooks flow

Before starting, it is important to understand the flow of react hooks.

Here is a diagram that explains it visually:

hooks-flow

Note: The most important thing here is to notice how the "Run Effects" phase is executed last.

Here is a code example that explains this flow:

export const MyComponent: FC = () => {
  // 1. Run Lazy initializers (i.e. () => 0)
  const [state, setState] = useState(() => 0);

  const previousStateRef = useRef();

  useEffect(() => {
    // 3. Run effect
    previousStateRef.current = state;
  }); // no deps array, because we want this to be called on every render

  // 2. Render
  return (
    <>
      Prev: {previousStateRef.current ?? "undefined"}
      <br />
      Current: {state}
      <br />
      <button onClick={() => setState(1)}>Update</button>
    </>
  );
}

In the "Mount" phase you will see:

Prev: undefined
Current: 0

This is because useEffect is called after the first render and because assigning a value to ref does not trigger a re-render - instead, the value is populated and waiting for the next update phase.

When button is clicked, React will trigger the update phase and the result will be:

Prev: 0
Current: 1

You can try this example out in codesandbox:

Using hook dependency arrays in useEffect

Dependency arrays are a way to trigger re-firing of hooks only when their dependencies change. React will keep the values generated by a hook stored in memory and then run it when a change of a dependency has occurred. This applies to all hooks that can have dependencies.

export const MyComponent: FC = ({ numberProp, stringProps }) => {
  useEffect(() => {
    // Runs only when `numberProps` or `stringProp` changes
  }, [numberProp, stringProp])

  // ...
}

For non-primitive values like objects, arrays and functions, React will do a reference comparison using Object.is(). This means that we must provide the same reference of objectProp, arrayProp, functionProp to Child on every re-render if we don't want to trigger useEffect every time. This can be achieved by extracting them outside of the component or wrapping them in useMemo or useCallback. Check Using Object.is section on MDN for better understanding.

Note: There is a JavaScript Records & Tuples Proposal which will invalidate above arguments and make things simpler.

const Child: FC = ({ objectProp, arrayProp, functionProp }) => {
  useEffect(() => {
    // Runs only when `objectProp`, `arrayProp` or `functionProp` reference changes
  }, [objectProp, arrayProp, functionProp])

  return ...;
}

// BAD
const Parent: FC = () => {
  // here we could have some code unrelated to Child component that could trigger re-render of Parent

  return (
    <Child
      objectProp={{ a: 'a' }}
      arrayProp={['a']}
      functionProp={() => 'a'}
    />
    // some other components...
  );
}

// GOOD
const objectProp = { a: 'a' };
const arrayProp = ['a'];

const Parent: FC = () => {
  // here we could have some code unrelated to Child component that could trigger re-render of Parent

  const functionProp = useCallback(() => 'a', []);

  return (
    <Child
      objectProp={objectProp}
      arrayProp={arrayProp}
      functionProp={functionProp}
    />
    // some other components...
  );
}

// FUTURE with Records & Tuples
const Parent: FC = () => {
  const functionProp = useCallback(() => 'a', []);

  return (
    <Child
      objectProp={#{ a: 'a' }}
      arrayProp={#['a']}
      functionProp={functionProp}
    />
  );
}

Use an empty dependency array if you want a hook to fire only on initial render.

export const MyComponent: FC = () => {
  useEffect(() => {
    // Runs only on initial render
  }, [])

  // ...
}

If you don't provide a dependency array at all, useEffect will be called on mount and on each update. For example, this could be used for storing previous values or sending events to an analytics service on each state change.

export const MyComponent: FC = () => {
  const [state, setState] = useState(() => 0);
  const prevRef = useRef();

  useEffect(() => {
    // runs on mount and on each update
    prevRef.current = state;
  })

  // ...
}

You may think to yourself: "Why can't we just do it without useEffect?. What's the difference?".

There is an upcoming React feature called Concurrent Mode, so we can consider this approach as kind of a preparation for that feature. So, we don't want to call a function directly because it could slow down or even prevent things from rendering. We want to offload the Side Effects somewhere else and leave the "main thread" intact and ready to execute.

Useful links: 1. A Complete Guide to useEffect

Avoid misusing hook dependencies

As all values and references are recreated on each render, if you use anything from inside the component scope, be sure to provide it as a dependency, even if you think that the dependency will never change.

For example, when a useEffect includes a prop or a value created inside the component's scope, you might be tempted to pass an empty array as a dependency to run it only on the initial render (trying to use it as you would componentDidMount in class components):

// BAD
export const MyComponent: FC = () => {
  const [value, setValue] = useState();

  const updateState = () => {
    // Do something with `setValue`
  }

  useEffect(() => {
    updateState();
  }, [])

  // ...
}
// BAD
export const MyComponent: FC = ({ handlerFunction }) => {
  useEffect(() => {
    handlerFunction();
  }, [])

  // ...
}

Or you might want to skip some of the dependencies that you think will not change:

// BAD
const MyComponent: FC = () => {
  const [value, setValue] = useState(null);

  const doSomethingOnValueChange = () => {...}

  useEffect(() => {
    if (value) {
      doSomethingOnValueChange(numberProp);
    }
  }, [value]) // missing a dependency (`doSomethingOnValueChange`)

  // ...
}

While you might think that a value will never change, it is not guaranteed, and React could use a stale value or reference, which might introduce bugs that are hard to trace in the future.

If you are doing this, there is a good chance that you should rethink the implementation and you actually need an alternative approach.

Derive new state from the previous state value, if possible

This way, you can avoid using state values as hook dependencies and causing additional re-renders.

// BAD
const Counter: FC = ({ incrementBy = 1 }) => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => setCount(count + incrementBy), [
    incrementBy,
    count,
  ])

  // ...
}
// GOOD
const Counter: FC = ({ incrementBy = 1 }) => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => setCount((previousCount) => previousCount + incrementBy), [incrementBy])

  // ...
}

Using lazy initialization when setting initial state with functions

If you are setting state with a function call, use lazy initialization and wrap it in an arrow function. This way you ensure that the function will be invoked only on first render:

// BAD
const [id, setId] = useState(generateId());
// GOOD
const [id, setId] = useState(() => generateId());

If the initial state is undefined, specify the state type:

// BAD
const [text, setText] = useState();
// GOOD
const [text, setText] = useState<string>();

Declare functions outside of the component, when they don't use anything from component scope (if you need to optimize rendering)

const onButtonClick = (e: SyntheticEvent<HTMLButtonElement>) => {
  // This event handler is using only DOM's `event`
}

const Button: FC = () => {
  // ...
}
const changeDocumentTitle = (title) => {
  document.title = title;
}

const App: FC = () => {
  // ...
}

Add event listeners in useEffect

If you need to add event listeners from component scope, you can do it inside useEffect. Don't forget to cleanup in the return statement, which will execute before component is unmounted.

const App: FC = () => {
  useEffect(() => {
    const handler = () => ...;

    document.addEventListener('fullscreenchange', handler);

    return () => {
      document.removeEventListener('fullscreenchange', handler);
    }
  }, []);

  // ...
}

Note: cleanup is always a function.

Wrap values derived from expensive calculations with useMemo

If you are using an expensive calculation to assign a value and it is causing slower performance, you can utilize useMemo to trigger that calculation only when necessary.

// BAD
export const MyComponent: FC = ({ propA, propB }) => {
  const value = someExpensiveCalculation(propA, propB);

  // ...
}
// GOOD
export const MyComponent: FC = ({ propA, propB }) => {
  const memoizedValue = useMemo(() => someExpensiveCalculation(propA, propB), [propA, propB]);

  // ...
}

Defer creation of non-primitive values if you are using them within a dependency array

Since objects will be recreated on each render, they will also have a different reference every time - and useEffect will treat it as a changed dependency.

// BAD
export const MyComponent: FC = ({ propA, propB }) => {
  const shape = {
    a: propA,
    b: propB,
  };

  useEffect(() => {
    /**
     * On each render, `useEffect` is causing an additional re-render,
     * since it's dependency is an object with a new reference every time.
     */
    doSomeSideEffectsWithShape(shape);
  }, [shape])

  // ...
}

Assuming that propA and propB are primitive (or memoized, non-primitive) values, with useMemo you are ensuring that non-primitive references are retained throughout re-renders, until propA or propB have changed.

export const MyComponent: FC = ({ propA, propB }) => {
  useEffect(() => {
    // Triggers re-renders only if propA and propB have changed
    const shape = {
      a: propA,
      b: propB,
    };

    doSomeSideEffectsWithShape(shape);
  }, [propA, propB])

  // ...
}

Wrap non-primitive values with useMemo, if you are sending them as props

You can use useMemo if you need to pass an non-primitive value as a prop to a component and you know it will cause a large subtree to re-render each time it changes:

export const MyDataChartWithIntersection: FC = ({ xAxisData, yAxisData }) => {
  // getChartData is expensive calculation
  const data = useMemo(() => getChartData(xAxisData, yAxisData), [xAxisData, yAxisData]);

  // getIntersections is expensive calculation
  const intersection = useMemo(() => getIntersections(xAxisData, yAxisData), [xAxisData, yAxisData])

  return (
    <Chart data={data} intersection={intersection}/>
  );
}

Do not use useMemo as a semantic guarantee that it will be a constant throughout component re-renders

If you need a value to stay the same throughout re-renders, you might think of useMemo as a nice way to "mimic" a constant. While it might seem that way, it is not guaranteed.

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for off-screen components. Write your code so that it still works without useMemo — and then add it to optimize performance when it's necessary.

To see what the alternatives are, check out the keeping consistent values trough rerenders recipe.

Two-pass rendering

If you intentionally need to render something different on the server or on the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like isClient, which you can set to true in useEffect (since useEffect only runs on the client). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.

Remember to be mindful of user experience on slow connections. The JavaScript code may load significantly later than the initial HTML render, so if you render something different in the client-only pass, the transition can be jarring. However, if executed well, it may be beneficial to render a “shell” of the application on the server, and only show some of the extra widgets on the client. To learn how to do this without getting the markup mismatch issues, refer to the explanation in the imperative update recipe.

const useIsClient = () => {
  const [isClient, setClient] = useState(false);

  useEffect(() => {
    setClient(true)
  }, []);

  return isClient;
}

export const Component: FC = () => {
  const isClient = useIsClient();

  return <div>{isClient ? 'Client' : 'Server'}</div>
}

Wrap functions with useCallback if they can cause large subtrees to re-render too often

While it is mostly unnecessary to wrap functions with useCallback, since the memoization process will usually be as expensive or more, it can be useful if functions' dependencies change often or if they are passed down to a large subtree of children.

// In this case, memoization is probably not worth it, since it will not necessarily improve performance
export const MyButton: FC = () => {
  const onClick = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
    ...
  }, [])

  return (
    <Button onClick={onClick} />
  );
}
// Here, memoization is probably useful, because `onClick` will stay the same if other state changes re-render the component, and will not trigger a large subtree to re-render unnecessarily
export const MyList: FC = () => {
  // ...

  const onListItemClick = useCallback(() => {
    ...
  }, [...])

  return <LargeList onListItemClick={onListItemClick} />;
}

You don't have to avoid "inlining" non-expensive functions

React is good at optimizing, so if you prematurely decide to wrap a function inside a useCallback, often you will end up with slower performance. This is because memoization is expensive. In most cases, even if you improve performance, it will be an insignificant gain compared to simple and maintainable code you had before.

export const StepButton: FC = ({ onClick, stepSize }) => {
  /**
   * You don't have to optimise (wrap in useCallback) because onClick and stepSize
   * are the same on each render and React can optimise this by itself.
   */
  return <button onClick={() => onClick(stepSize)}>Increment for {stepSize}</button>;
}

// GOOD
export const MyComponent: FC = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);

  return (
    <div>
      <div>Count {count}</div>
      <StepButton onClick={increment} stepSize={2} />
    </div>
  );
}

Hooks encapsulation

A common problem with hooks is that the components using them can go out of control and become unreadable and messy. This happens if you have multiple invocations of useEffect and useCallback in your function component body, which happens often if you are building real world products. To overcome this problem we can do the same thing as we would do in class components - split things into smaller chunks of logic and extract them, i.e. private methods of class components or custom hooks in function components.

The Problem:

This is a continuation of the previous section, but we will expand on it with some additional requirements. We need to add an input field for setting the description of the value that we are counting, with decrement and reset handlers for the counter.

This is the previous code:

export const MyComponent: FC = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);

  return (
    <div>
      <div>Count {count}</div>
      <StepButton onClick={increment} stepSize={2} />
    </div>
  );
}

This is the updated code with additional features.

export const MyComponent: FC = () => {
  const [count, setCount] = useState(0);
  const [inputState, setInputState] = useState('')

  const increment = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);
  const decrement = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);
  const reset = useCallback(() => { setCount(0) }, []);

  const handleInputChange = useCallback(e => {
    setInputState(e.target.value);
  }, []);

  return (
    <div>
      <div>
        <label htmlFor="entity">Entity</label>
        <input
          type="text"
          id="entity"
          onChange={handleInputChange}
          value={inputState}
        />
      </div>
      <div>Count {count}</div>
      <StepButton onClick={increment} stepSize={2} />
      <StepButton onClick={decrement} stepSize={-2} />
      <button onClick={reset}>Reset</button>
    </div>
  );
}

You can see that our component becomes messy and hard to understand. There is a lot of code in the body of the function which increases our cognitive load. To fix this issue we can encapsulate our elements of concern into a separate chunks of work, i.e. custom hooks.

The Solution:

const useCount = (initialState = 0) => {
  const [state, setState] = useState(() => initialState);

  const handlers = useMemo(
    () => ({
      increment: (stepSize = 1) => {
        setState(previousCount => previousCount + stepSize)
      },
      decrement: (stepSize = -1) => {
        setState((previousCount) =>  previousCount + stepSize)
      },
      reset: () => {
        setState(0)
      }
    }),
    []
  );

  return [state, handlers];
}

const useInput = (initialState = '') => {
  const [state, setState] = useState(() => initialState);

  const handlers = useMemo(() => ({
    handleInputChange: (event) => {
      setState(event.target.value);
    }
  })), []);

  return [state, handlers]
}

export const MyComponent: FC = () => {
  const [count, { increment, decrement, reset }] = useCount();
  const [inputState, { handleInputChange }] = useInput();

  return (
    <div>
      <div>
        <label htmlFor="entity">Entity</label>
        <input
          type="text"
          id="entity"
          onChange={handleInputChange}
          value={inputState}
        />
      </div>
      <div>Count {count}</div>
      <StepButton onClick={increment} stepSize={2} />
      <StepButton onClick={decrement} stepSize={-2} />
      <button onClick={reset}>Reset</button>
    </div>
  );

Useful links: 1. useEncapsulation or Why Your React Components Should Only Use Custom Hooks 2. Encapsulation or the Primary Purpose of Functions

Before you use memoization

Have in mind that React is really good at optimizing re-renders by default.

You might get tempted to wrap values and functions with useMemo and useCallback all the time, but in many of these cases, you don't really need it, and you might even make your app performance and file size worse. These calculations can be expensive and you could end up using more memory than you would without them and make your code more complicated to read and maintain.

If it's not obvious that memoization is needed, profile your app performance without it first, using React Devtools, and then optimize if necessary.

React Devtools Profiler React Devtools Profiler


If you want to dive deeper here are some useful articles: 1. When to useMemo and useCallback 2. One simple trick to optimize React re-renders 3. Profile a React App for Performance 4. React Production Performance Monitoring