Lazy initialization is passing a function instead of a value to useState/useReducer, which executes ONLY on the first render.
Why it matters: Without lazy initialization, expensive computations run on every single render, even though the result is only needed once. This is a common performance pitfall in React applications. For example, parsing a large JSON string from localStorage, generating a complex data structure, or running a cryptographic operation — all of these waste CPU cycles if executed on every render.
Problem: Without lazy initialization, the initial value calculation executes on EVERY render:
1// BAD: expensiveCalculation() is called on every render2const [data, setData] = useState(expensiveCalculation());
How it works step-by-step:
useState(() => value) during the first render.Solution — pass a function:
1// GOOD: expensiveCalculation() is called ONCE2const [data, setData] = useState(() => expensiveCalculation());34// Example: initial value from localStorage5const [theme, setTheme] = useState(() => {6 const saved = localStorage.getItem("theme");7 return saved || "light"; // JSON.parse only on mount8});910// Example: complex calculation11const [matrix, setMatrix] = useState(() => generateMatrix(100));
Lazy initialization with useReducer:
1function reducer(state, action) { /* ... */ }23const [state, dispatch] = useReducer(reducer, null, () => {4 return loadInitialState(); // Executes once5});
Real-world example — loading user preferences:
1function App() {2 const [preferences, setPreferences] = useState(() => {3 try {4 const raw = localStorage.getItem("userPrefs");5 return raw ? JSON.parse(raw) : getDefaultPreferences();6 } catch {7 return getDefaultPreferences();8 }9 });1011 return <SettingsPanel prefs={preferences} />;12}
Performance considerations:
Common mistakes:
useState(getValue()) vs useState(() => getValue()).Date.now()) — this would freeze the stale value.Rule: Always pass a function if the initial value is expensive to compute or read from an external source (localStorage, API, file).