Passing a function as initial state in useState
The Short Answer
When you pass a function to useState() instead of a value, React calls that function only once — during the initial render — and uses its return value as the initial state. This is called lazy initialization. It's useful when computing the initial state is expensive (parsing JSON, reading from localStorage, complex calculations) because it avoids re-running that computation on every re-render.
The Problem It Solves
When you pass a value directly to useState, that expression is evaluated on every render — even though React only uses it on the first render and ignores it afterward. For simple values like useState(0) this is negligible. But for expensive computations, you're paying the cost on every render for no benefit.
function TodoList() {
// ❌ This runs JSON.parse on EVERY render, even though
// React only uses the result on the first render
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem('todos') || '[]')
);
// ❌ This creates a new Date object on every render — wasted work
const [startTime, setStartTime] = useState(new Date());
// ❌ This filters a large array on every render
const [activeUsers, setActiveUsers] = useState(
allUsers.filter(user => user.isActive)
);
return <div>{/* ... */}</div>;
}
In the example above, JSON.parse(localStorage.getItem(...)) runs on every single render. React discards the result after the first render, but the parsing work still happens. With hundreds of todos or frequent re-renders, this becomes a measurable performance issue.
The Solution: Lazy Initializer Function
By wrapping the expensive computation in a function, you tell React: 'call this function once to get the initial value, then never call it again.' React stores the result and ignores the initializer on subsequent renders. The function receives no arguments and should return the initial state value.
function TodoList() {
// ✅ The function only runs on the FIRST render
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
// ✅ Date object created only once
const [startTime, setStartTime] = useState(() => new Date());
// ✅ Filtering only happens on initial render
const [activeUsers, setActiveUsers] = useState(
() => allUsers.filter(user => user.isActive)
);
return <div>{/* ... */}</div>;
}
The syntax difference is subtle but important: useState(value) evaluates value every render; useState(() => value) evaluates the function only once. React internally checks if the argument is a function — if it is, it calls it on mount and caches the result.
When to Use Lazy Initialization
Use a function initializer when:
- ✅Reading from localStorage or sessionStorage (synchronous I/O)
- ✅Parsing JSON strings (especially large ones)
- ✅Computing derived data from props or external sources
- ✅Creating complex initial objects (deep cloning, array generation)
- ✅Any computation that takes more than trivial time
A plain value is fine when:
- ✅Initial state is a primitive: useState(0), useState(''), useState(false)
- ✅Initial state is a simple literal: useState([]), useState({})
- ✅The computation is trivial: useState(props.defaultValue)
Common Mistake: Passing a Function Call
A frequent mistake is accidentally calling the function instead of passing it. The difference between useState(getInitial) and useState(getInitial()) is critical — the first passes the function (lazy), the second calls it immediately (eager, runs every render).
function getExpensiveDefault() {
console.log('Computing...');
return heavyComputation();
}
function MyComponent() {
// ✅ Passes the function — React calls it once
const [value, setValue] = useState(getExpensiveDefault);
// Console: "Computing..." (only on first render)
// ❌ Calls the function — runs every render
const [value2, setValue2] = useState(getExpensiveDefault());
// Console: "Computing..." (on EVERY render)
// ✅ Arrow function wrapper — also works for functions that need arguments
const [value3, setValue3] = useState(() => computeWith(props.id));
}
When your initializer needs arguments (like props), you must use an arrow function wrapper since React calls the initializer with no arguments. useState(() => compute(props.id)) is the correct pattern.
Why Interviewers Ask This
This question tests whether you understand React's rendering model — specifically that component functions run on every render and that useState's argument is evaluated each time even though only the first result matters. It shows you think about performance at the hook level and know the difference between passing a value and passing a function.
Quick Revision Cheat Sheet
Syntax: useState(() => expensiveComputation()) — function called once on mount
Why: Avoids re-running expensive initialization on every re-render
Common use: localStorage reads, JSON parsing, complex object creation
Gotcha: useState(fn) passes function (lazy); useState(fn()) calls it (eager)
With args: useState(() => compute(props.id)) — wrap in arrow function
Skip when: Initial value is a simple primitive or literal — overhead is negligible