In one line, useState in React lets a functional component store and update state values that trigger re-renders when changed.
useState = what changes
useEffect = what happens when it change
What Is “State” in a React Component?
In simple terms, a state is the data that determines how your component behaves. So if HTML is a template of UI, then the data is the state.
When the state changes, React re-renders the component so the UI always stays in sync with that data.
Before You Begin
If you’re new to React’s useState , check out my earlier article first: Understanding useState in React
It covers the basics, what useState is how to declare and update state, and how React re-renders components when state changes.
In this article, let's go a step further.
We’ll explore advanced useState concepts :
How state initialization works only once
Why can’t you use it inside loops or conditionals
The different ways to initialize the state
Using Objects in State
Function Calls Inside useState
Lazy Initialization
Clicked Twice, Still +1?
How state initialization works only once
useState hooks into React’s internal state machine.
On the first render: The default value is evaluated, and a state cell is created.
On re-renders: React reuses the same cell, ignoring the initializer .
function Counter() {
const [count, setCount] = useState(() => {
console.log("Initializer runs once");
return 0;
});
console.log("Component rendered");
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
Output in console:
“Initializer runs once” // only once
“Component rendered” // on every render -> Button click
Why You Can’t Use useState Inside Loops or Conditions
React needs to maintain a consistent order of hooks between renders.
function Example() {
if (true) {
const [data, setData] = useState(0); // You'll get following compile time error
}
}
![if UseState Error]()
On one render, useState may run.
On another, it may not.
React’s internal hook order mismatches → it throws “Invalid hook call” error.
Always call hooks at the top level of your component, not inside loops, conditions, or nested functions.
function Example({ condition }) {
const [data, setData] = useState(0);
return condition ? <div>{data}</div> : null;
}
Different Ways to Initialize State
Simple Value
const [count, setCount] = useState(0);
From Props
function Profile({ defaultName }) {
const [name, setName] = useState(defaultName);
}
From Expensive Computation (with lazy initializer)
const [data, setData] = useState(() => getDataFromLocalStorage());
Tip: Using a function delays computation until the component first mounts, avoids unnecessary recalculations on re-renders.
Using Objects in State
const [user, setUser] = useState({ name: "", age: 0 });
function updateName(newName) {
setUser({ name: newName }); // ❌ This removes `age` from state
}
// Rather spread the object
setUser(prev => ({ ...prev, name: newName }));
When using objects, you must always spread the previous state.
If you find yourself doing that too much, consider separate states.
const [name, setName] = useState("");
const [age, setAge] = useState(0);
Rule of thumb
Function Calls Inside useState
// Called only once
function getDefaultTheme() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
const [theme, setTheme] = useState(getDefaultTheme);
Here getDefaultTheme is passed by reference, not called immediately. React calls it once to compute the initial state.
Lazy Initialization
Sometimes you want to recalculate the state when certain props change, not every render.
function DerivedExample({ value }) {
const [processed, setProcessed] = useState(() => process(value));
useEffect(() => {
setProcessed(process(value));
}, [value]);
}
Clicked Twice, Still +1?
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
You might expect the count to go up by 2 every time you click. But it only increases by 1.
In React, state updates are asynchronous and batched for performance.
When you call setCount(count + 1) , React captures the current count ( 0 ), computes the new value ( 1 ), and schedules that update.
The second setCount(count + 1) runs in the same render, so it also uses the old count value ( 0 ) it doesn’t see the first update yet.
Result: both updates calculate 1 , and that’s the final state.
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
Now the count increases by 2 with every click.