React  

Understanding Advanced Patterns of useState in React

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.

  1. On the first render: The default value is evaluated, and a state cell is created.

  2. 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

  1. Simple Value

  
    const [count, setCount] = useState(0);
  
  1. From Props

  
    function Profile({ defaultName }) {
  const [name, setName] = useState(defaultName);
}
  
  1. 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

  • Few unrelated values: use separate states.

  • Closely related or dynamic data: use an object (but always spread correctly).

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.

  • Each updater function receives the latest state value (after any pending updates).

  • So the second setCount uses the already incremented value from the first one.