Skip to main content

The `useState` Hook: A Deep Dive (Part 2) #50

📖 Introduction

In the previous article, we learned the fundamentals of the useState hook. We know how to declare a state variable and use its setter function to trigger re-renders. Now, we'll explore a more advanced and optimized way to set the initial state.

This article covers "lazy initialization" with useState. You'll learn how to pass a function to useState to prevent expensive calculations from running on every render, ensuring your application starts up as efficiently as possible.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • The useState Hook: You must be comfortable with the basic usage of useState.
  • JavaScript Functions: You should understand that functions can be passed as arguments to other functions.
  • Component Renders: A conceptual understanding that a component function re-runs when its state or props change.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Problem with Expensive Initial State: Understanding why calling a slow function to get an initial value can be inefficient.
  • Lazy Initialization: The pattern of passing an "initializer function" to useState.
  • How It Works: Why React only calls the initializer function once, on the very first render.
  • The Syntax: The crucial difference between useState(myFunction()) and useState(myFunction).

🧠 Section 1: The Core Concept: The Problem with Re-running Initializers

Let's consider how useState works.

const [myState, setMyState] = useState(initialValue);

React only uses initialValue on the very first render. On every subsequent re-render, it ignores this argument and instead provides the current value of myState.

Now, imagine that calculating the initial value is slow or "expensive."

function createInitialTodos() {
// Imagine this function is very slow...
// It might loop 10,000 times or read from localStorage.
console.log('Calculating initial todos...');
return [...Array(10000).keys()].map(i => ({ id: i, text: `Todo ${i}`}));
}

function TodoList() {
// This calls createInitialTodos() on EVERY render!
const [todos, setTodos] = useState(createInitialTodos());
// ...
}

In this example, even though React only uses the result of createInitialTodos() on the first render, the function itself is still called on every single re-render of TodoList. This is wasteful and can slow down your component.


💻 Section 2: The Solution: Lazy Initialization

To solve this, useState has a special feature: if you pass a function as the initial state, React will only execute that function once, during the initial render, to get the initial value. This is called "lazy initialization."

The Syntax: You pass the function reference itself, without calling it.

Incorrect (Eager Initialization): useState(createInitialTodos()) -> This calls the function immediately and passes its return value. The function runs on every render.

Correct (Lazy Initialization): useState(createInitialTodos) -> This passes the function itself. React will only call this function once, on the first render.

Let's fix our TodoList component.

// code-block-1.jsx
import React, { useState } from 'react';

function createInitialTodos() {
// This is an expensive calculation that we only want to run once.
console.log('Calculating initial todos...');
const initialTodos = [];
for (let i = 0; i < 50; i++) { // Reduced for the example
initialTodos.push({ id: i, text: 'Item ' + (i + 1) });
}
return initialTodos;
}

export default function TodoList() {
// Pass the function itself, not the result of calling it.
const [todos, setTodos] = useState(createInitialTodos);
const [text, setText] = useState('');

return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
setTodos([{ id: todos.length, text: text }, ...todos]);
}}>Add</button>
<ul>
{todos.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</>
);
}

Now, if you run this code and type in the input field, you will see that "Calculating initial todos..." is only logged to the console once. The component re-renders every time you type, but the expensive initializer function is no longer being called unnecessarily.


✨ Section 3: When to Use Lazy Initialization

You don't need to use this pattern for every useState call. It's specifically for situations where the initial state is derived from an expensive calculation.

Good use cases:

  • Reading from localStorage or sessionStorage, which can be slow.
  • Parsing a large piece of data (e.g., a large JSON string).
  • Performing a complex, synchronous calculation or loop to generate the initial data.

For simple initial values like 0, '', true, or a small, hardcoded array, there is no performance benefit to using an initializer function.


💡 Conclusion & Key Takeaways

Lazy initialization with useState is a valuable optimization technique for ensuring your components initialize efficiently, especially when the initial state requires a heavy computation.

Let's summarize the key takeaways:

  • Initializer Functions Run Once: If you pass a function to useState, React will only call it during the initial render to get the state's value.
  • Pass the Function, Don't Call It: The syntax is useState(myFunction), not useState(myFunction()).
  • Use for Expensive Calculations: This pattern is ideal when creating the initial state is a slow process, such as reading from localStorage or performing a complex loop.
  • Not Needed for Simple Values: For simple, primitive initial values, there's no need for an initializer function.

Challenge Yourself: Imagine you have a component that gets its initial theme ('light' or 'dark') from localStorage. Write a getInitialTheme function that reads this value. Then, create a ThemeSwitcher component that uses useState with your getInitialTheme function to lazily initialize its theme state.


➡️ Next Steps

You've now learned the key aspects of initializing state. In the next article, we'll continue our deep dive into useState by exploring how to correctly update state that is based on the previous state.

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • Lazy Initialization: The practice of deferring an expensive computation so that it only runs when it's absolutely necessary. In useState, this means passing an initializer function that React only calls once.
  • Initializer Function: A function passed to useState that React executes only on the initial render to compute the initial state.
  • Expensive Calculation: Any computation that takes a noticeable amount of time to complete, potentially slowing down the rendering of a component.

Further Reading