The importance of lazy initialization with useState
5 min readAs a React developer, I use useState
constantly to manage state in my functional components. However, there’s a powerful but lesser-known feature of this hook: lazy initialization. In this article, I’ll explore what it is, why it’s important, and how to implement it correctly.
What is lazy initialization in useState?
Lazy initialization is a technique that allows me to define the initial state of a component using a function instead of a direct value. React will execute this function only during the initial mount of the component, ignoring it in subsequent renders.
The syntax is straightforward:
// Standard initialization
const [state, setState] = useState(initialValue);
// Lazy initialization
const [state, setState] = useState(() => computeInitialValue());
Why is it important?
I’ve discovered that lazy initialization is especially useful when:
- The initial value requires expensive calculations
- I need to access localStorage or sessionStorage
- I’m performing an operation that shouldn’t be repeated on every render
Without lazy initialization, any operation in the useState
argument would run on every render, even though React only uses that value the first time.
Step-by-step example
Let’s look at a practical case where I’ve found that lazy initialization makes a difference:
Problem: Inefficient loading from localStorage
Imagine a component that needs to load its initial state from localStorage:
import React, { useState } from "react";
function UserProfile() {
// ⚠️ This code is inefficient
const [user, setUser] = useState(
JSON.parse(localStorage.getItem("user")) || { name: "", email: "" },
);
// Rest of the component...
return (
<div>
<h2>User Profile</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
{/* Edit form... */}
</div>
);
}
The problem I see here is that JSON.parse(localStorage.getItem('user'))
will run on every render, even if React only uses this value during the initial mount.
Solution: Implementation with lazy initialization
import React, { useState } from "react";
function UserProfile() {
// ✅ Correct lazy initialization
const [user, setUser] = useState(() => {
// This function only runs during the initial mount
const savedUser = localStorage.getItem("user");
if (savedUser) {
return JSON.parse(savedUser);
}
return { name: "", email: "" };
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser((prevUser) => ({
...prevUser,
[name]: value,
}));
};
const handleSave = () => {
localStorage.setItem("user", JSON.stringify(user));
alert("User saved successfully");
};
return (
<div>
<h2>User Profile</h2>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={user.name}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={user.email}
onChange={handleChange}
/>
</div>
<button onClick={handleSave}>Save changes</button>
</div>
);
}
Measuring the impact with an exaggerated example
To clearly demonstrate the impact, I’ve created an exaggerated example where I calculate something expensive:
import React, { useState } from "react";
// Simulated expensive function
const calculateExpensiveInitialState = () => {
console.log("⚙️ Calculating initial state (expensive operation)");
// Simulate a heavy calculation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
};
// Component without lazy initialization
function WithoutLazy() {
// ⚠️ The expensive function runs on every render
const [value, setValue] = useState(calculateExpensiveInitialState());
return (
<div>
<p>Value: {value}</p>
<button onClick={() => setValue(Math.random())}>Update value</button>
</div>
);
}
// Component with lazy initialization
function WithLazy() {
// ✅ The expensive function only runs once during the mount
const [value, setValue] = useState(() => calculateExpensiveInitialState());
return (
<div>
<p>Value: {value}</p>
<button onClick={() => setValue(Math.random())}>Update value</button>
</div>
);
}
In my tests, if I run these components and check the console:
WithoutLazy
shows the “Calculating initial state” message every time the component rendersWithLazy
shows the message only once during the initial mount
When do I use lazy initialization?
In my experience, I use lazy initialization when:
- The initial value requires expensive calculations
- I need to read from localStorage/sessionStorage
- I’m performing complex data transformations
- I need to generate random values that should remain consistent
I don’t usually use lazy initialization when:
- The initial value is simple (number, string, boolean)
- The initial value is a constant reference (an empty object or array like
[]
or{}
)
Conclusion
Lazy initialization is a simple but powerful technique that I’ve incorporated to optimize the performance of my React components. I always use it when:
- The calculation of the initial state is expensive
- I don’t want the initialization logic to run on every render
It’s a simple pattern that has made a big difference in the performance of my applications, especially in components that handle complex data or interact with browser storage.