The React useState hook and lazy initialization
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
.