Introduction
When building React applications, one of the most fundamental tools we use, is component state management through the useState hook. In most cases, we directly assign an initial value to our state, but this approach isn't always the most efficient solution. Sometimes, computing that initial value can be an expensive operation or simply unnecessary to repeat on every render.
For these specific scenarios, React provides lazy initialization with useState: a simple yet powerful technique that can significantly improve your application's performance.
Understanding Lazy Initialization
The Problem
In typical usage, you might write:
const [count, setCount] = useState(expensiveCalculation());
Here's the issue: expensiveCalculation() executes on every render, even though we only need its result once. React ignores the value after initialization, but the function still runs, wasting resources.
The Solution
With lazy initialization, pass a function instead:
const [count, setCount] = useState(() => expensiveCalculation());
Now React executes expensiveCalculation() only once during the initial render and never again.
When to Use It
Use lazy initialization when:
- Reading from external sources: localStorage, sessionStorage, cookies, or the DOM
- Expensive computations: Parsing large JSON, complex calculations, or processing big datasets
- Complex data structures: Building intricate initial configurations or nested objects
Don't use it for simple values like useState(0), useState(""), or useState([]). The overhead isn't worth it.
Real-World Example: Theme Persistence
import { useEffect, useState } from "react";
function App() {
const [theme, setTheme] = useState<string>(
() => localStorage.getItem("theme") || "light"
);
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
return (
<div
style={{
backgroundColor: theme === "dark" ? "#222" : "#fff",
minHeight: "100vh",
}}
>
<button onClick={() => setTheme("light")}>Light Theme</button>
<button onClick={() => setTheme("dark")}>Dark Theme</button>
</div>
);
}
export default App;
Why Use Lazy Initialization?
Without it:
// β localStorage.getItem runs on EVERY render
const [theme, setTheme] = useState(localStorage.getItem("theme") || "light");
With it:
// β
localStorage.getItem runs ONLY ONCE
const [theme, setTheme] = useState(() => localStorage.getItem("theme") || "light");
Benefits
- Performance: Eliminates redundant localStorage reads on every re-render
- Efficiency: Avoids synchronous I/O operations that slow down rendering
- Clarity: Explicitly shows initialization-only logic
Without Lazy Initialization
Every time the component re-renders (button clicks, parent updates), you're:
- Performing unnecessary localStorage reads
- Wasting CPU cycles on discarded results
- Degrading performance in frequently re-rendering components
Conclusion
Lazy initialization with useState is a targeted optimization for expensive or external initialization operations. Use it when reading from localStorage, performing heavy calculations, or building complex data structures.
Remember: Apply it only where it matters. Don't over-optimize simple values, but don't ignore it when dealing with costly operations.
Top comments (1)
Such an amazing article! Thanks for sharing.