Lazy state initialization with React

Lazy state initialization with React

ยท

5 min read

If you are familiar with the useState hook in React, you know that it's a function that's called each time our component renders.

The function returns an array of two values, and we usually use JavaScript destructuring to both declare a variable for our state, and define a function which updates our state. Something like the following is quite common.

const [count, setCount] = React.useState(0);

We typically prefix our function with set followed by the name of our state variable. You'll notice that we are passing the number 0 to useState, this means that the initial value of count will be 0.

As the value passed to useState is our "initial" value, we only need it when the component first renders. Earlier, I mentioned useState runs on each render, but if we update count, that causes useState to get our initial value when it's not necessary.

Just passing a number to useState isn't too expensive in terms of performance, but what if we are doing something more demanding like reading the value from local storage.

Let's look at the following code. In this example, we have a component which updates our state by adding 1 to the count each time a button is clicked. Nothing too exciting. You'll notice there is a console.log("rendering");. If you open the dev tools, you'll see the log each time you click on the button.

const Counter = () => {

    console.log("rendering");

    const [count, setCount] = React.useState(0);

    function handleIncrement(e) {
        e.preventDefault();
        setCount(count + 1);
    }

    return (
        <div>
            <p>{count}</p>
            <button onClick={handleIncrement}>+</button>
        </div>
    )
}

Let's update our component to store our state in local storage. We use useEffect to write to local storage as it runs each time after React renders the component to the DOM.

const Counter = () => {

    console.log("rendering");

    const [count, setCount] = React.useState(Number(localStorage.getItem('count')) || 0);

    function handleIncrement(e) {
        e.preventDefault();
        setCount(count + 1);
    }

    React.useEffect(() => {
        localStorage.setItem('count', count);
    })

    return (
        <div>
            <p>{count}</p>
            <button onClick={handleIncrement}>+</button>
        </div>
    )
}

Now that we are doing something more demanding, wouldn't it be great if there was a way to tell useState just to get our "initial" state the first time? Luckily for us, there is. We can use lazy state initialization simply by passing useState a function instead of a number.

We can adapt part of our code to the following. You can see we added a second console.log from inside our new function.

    console.log("rendering");

    function readLocalStorage() {
        console.log('rendering from lazy state function');
        return Number(localStorage.getItem('count')) || 0;
    }

    const [count, setCount] = React.useState(readLocalStorage);

If you take a look at the console, you will see "rendering" and "rendering from lazy state function" when the component first renders, but only "rendering" when you click the button to increment. That means our function is still defined every render, but setState got our initial value only once. Defining the function every render is still cheaper in terms of performance.

That's basically everything. but we can still make one small improvement to our code. It was nice to see the log messages to help us understand the functionality, but we don't need to define a function like above, we can just pass one inline instead.

const [count, setCount] = React.useState(() => Number(localStorage.getItem('count')) || 0);

This is much prettier. We write an inline function, and now you have lazily initialized our state. Congratulations. ๐ŸŽ‰ Thanks for reading.

You can read more web musings on my twitter here.