The React useState hook and lazy initialization

September 18, 2022React

3 min read

React handles the initial state of components by saving it once, when the component first renders. Then, when the component re-renders, React ignores it. When the initialization of our state is computationally expensive, we must be careful how we define that initial state, or it can cause performance issues. Let's take a look at the following example.

const Cart = () => {
  const [products, setProducts] = useState(getUserProducts());
};

In the above example, the result of the getProducts() function call is only used during the initial render of the component. It initializes the state of products. However, getProducts() will still be called on every render. This can be a waste of resources if getProducts() is handling a lot of data or performing expensive calculations.

To avoid potential performance bottlenecks, we can instead pass an initializer function to useState.

const Cart = () => {
  const [products, setProducts] = useState(getUserProducts);
};

In this case, we are passing getUserProducts, which is the function itself, rather than getUserProducts(), which is the result of calling the function. If we pass a function to useState, as we did in this example, React will only call it once, during the first render of the Cart component.

We have just used lazy initialization to avoid the performance bottleneck of calling some expensive function on every render. If we were to put a console.log() statement in getUserProducts(), we would see it log to the console only on the initial render of the component.

This approach should only be used for computationally expensive state initialization that we don't want running on every component render. We shouldn't use lazy initialization for every state variable.

If the getUserProducts function depends on receiving function arguments, we would write it like this.

type Props = {
  userId: number;
};

const Cart = ({ userId }: Props) => {
  const [products, setProducts] = useState(() => getUserProducts(userId));
};

If we had used useState(getUserProducts(userId)), we would have been assigning the result of calling the function rather than the function itself. As a result, we used an anonymous function (a function without a name) within useState to call getUserProducts for us with the userId argument. This anonymous function then returns the result of the call to getUserProducts(userId) which useState uses to set the initial state for products.

Conclusion

When you are using a computationally expensive operation to initialize your state, such as loading data from an API or from localStorage, consider using a lazy initializer function with useState.