🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
REFERENCEreact

react Documentation

LOADING ENGINE...

useState

AI & DATA SCIENCE // usestate

useState is the hook that adds local, re-render-triggering state to a functional component, returning the current value and a setter function to update it.

Syntax

const [state, setState] = useState(initialValue);

Deep Dive Course

Calling useState(initialValue) returns an array with exactly two elements: the current state value and a function to update it, conventionally unpacked via array destructuring as [value, setValue]. The initialValue argument is only used on the very first render; on every subsequent render, useState returns whatever the current state actually is, ignoring the initial value passed in. Calling the setter schedules a state update and a subsequent re-render, but note the update isn't applied synchronously — the state variable inside the currently-executing function body still reflects the old value immediately after calling the setter, only the next render sees the new one.

1Understanding useState

Calling useState(initialValue) returns an array with exactly two elements: the current state value and a function to update it, conventionally unpacked via array destructuring as [value, setValue]. The initialValue argument is only used on the very first render; on every subsequent render, useState returns whatever the current state actually is, ignoring the initial value passed in. Calling the setter schedules a state update and a subsequent re-render, but note the update isn't applied synchronously — the state variable inside the currently-executing function body still reflects the old value immediately after calling the setter, only the next render sees the new one.

💡

Pass a function to useState, useState(() => expensiveComputation()), rather than calling that expensive computation directly, useState(expensiveComputation()), when the initial value is costly to compute — the function form only ever runs once, on the first render, while the direct form re-runs the expensive computation on every single render even though its result is discarded after the first.

editor.html
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);
  return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}
localhost:3000

2Practical Example

Here is a real-world application of useState showing how it is used in production React code.

editor.html
import { useState } from 'react';

function ExpensiveInit() {
  const [value] = useState(() => {
    console.log('Computing initial value...');
    return 42;
  });
  return <p>{value}</p>;
}
localhost:3000

3Best Practices

Follow these guidelines when working with useState:

1. Pass a function, not a direct value, to useState when the initial value requires an expensive computation, so it only runs once on mount

2. Use the functional updater form, setValue(prev => ...), whenever a new value depends on the current one, to avoid stale-value bugs

3. Split unrelated pieces of state into separate useState calls rather than combining everything into one large state object, for simpler, more targeted updates

⚠️

Tip: Pass a function to useState, useState(() => expensiveComputation()), rather than calling that expensive computation directly, useState(expensiveComputation()), when the initial value is costly to compute — the function form only ever runs once, on the first render, while the direct form re-runs the expensive computation on every single render even though its result is discarded after the first.

editor.html
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);
  return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}
localhost:3000

4The Functional Updater Form

When a new value depends on the previous one, pass a function to the setter instead of a direct value: setCount(prev => prev + 1). React guarantees this updater always receives the most current pending state, so it stays correct even when several updates are queued in the same event — calling setCount(count + 1) multiple times in a row reuses the same stale count each time, but the updater form correctly accumulates.

editor.html
// Unsafe with multiple calls
setCount(count + 1);
setCount(count + 1); // still uses old count

// Safe: always uses the latest pending value
setCount(prev => prev + 1);
setCount(prev => prev + 1);
localhost:3000

5Automatic Batching

React 18 batches every state update triggered inside the same event handler, promise callback, or timeout into a single re-render, no matter how many separate setter calls are made. This is a performance optimization that generally requires no extra code from you, but it explains why logging state right after calling a setter, several times in a row, always shows the same (stale) value within that render.

editor.html
function handleClick() {
  setA(1);
  setB(2);
  setC(3);
  // Still only ONE re-render
}
localhost:3000

6Immutability with Objects and Arrays

React detects a state change by comparing the new value to the old one with Object.is, which checks objects and arrays by reference. Mutating an object or array in place — pushing to an array, or assigning a property directly — leaves the reference unchanged, so React never sees a difference and skips the re-render. Always create a new object or array and pass that to the setter.

⚠️

This is one of the most common React bugs: mutating a state object or array directly, then calling the setter with that same, now-mutated reference — React still bails out of the re-render because Object.is sees no change.

editor.html
// Wrong: mutates in place, same reference
user.name = 'Bob';
setUser(user); // React sees no change

// Correct: new object, new reference
setUser({ ...user, name: 'Bob' });
localhost:3000

Examples

Example 01Basic Usage
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);
  return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}
Example 02Advanced Example
import { useState } from 'react';

function ExpensiveInit() {
  const [value] = useState(() => {
    console.log('Computing initial value...');
    return 42;
  });
  return <p>{value}</p>;
}
Example 03Updating Array State Immutably
import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([]);
  function addTodo(text) {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  }
  return <button onClick={() => addTodo('New task')}>Add</button>;
}

Best Practices

  • Pass a function, not a direct value, to useState when the initial value requires an expensive computation, so it only runs once on mount
  • Use the functional updater form, setValue(prev => ...), whenever a new value depends on the current one, to avoid stale-value bugs
  • Split unrelated pieces of state into separate useState calls rather than combining everything into one large state object, for simpler, more targeted updates
  • Always create a new object or array when updating object or array state, since React compares by reference and never detects an in-place mutation
  • Don't rely on reading the updated value immediately after calling a setter within the same function — the new value is only visible on the next render

Interview Question

Why does the state variable returned by useState still show the old value immediately after calling its setter function, within the same function execution?

Hint: Think about when exactly a functional component's variables are 'refreshed' with new values — during the current call, or only on the next one.

A functional component's local variables, including the value returned by useState, are just regular JavaScript variables scoped to that single function call/render — calling the setter function doesn't reach back in and mutate that already-executing function's variable in place, it instead schedules a new render, a fresh call to the component function, where useState will then return the updated value. Within the currently-running function call, the state variable is a fixed snapshot from when that render started, and no amount of calling the setter partway through that same execution changes what that already-captured variable currently holds — the new value only becomes visible the next time the component function actually runs again.

Exercises

MediumPractice using useState in a real scenario.
View Solution
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);
  return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}
MediumFix this counter so clicking the button once correctly increments the count by two, using the functional updater form.
View Solution
import { useState } from 'react';

function DoubleCounter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  }
  return <button onClick={handleClick}>{count}</button>;
}

Frequently Asked Questions

Why does the state variable returned by useState still show the old value immediately after calling its setter function, within the same function execution?

A functional component's local variables, including the value returned by useState, are just regular JavaScript variables scoped to that single function call/render — calling the setter function doesn't reach back in and mutate that already-executing function's variable in place, it instead schedules a new render, a fresh call to the component function, where useState will then return the updated value. Within the currently-running function call, the state variable is a fixed snapshot from when that render started, and no amount of calling the setter partway through that same execution changes what that already-captured variable currently holds — the new value only becomes visible the next time the component function actually runs again.

Why does calling setCount(count + 1) twice in the same handler only increment the count by one, not two?

Both calls read `count` from the same render's closure, so both see the same starting value — the second call doesn't know about the first call's pending update, it just recomputes `count + 1` from the same stale `count`, and the second update overwrites the first with an identical result. Using the functional updater form, setCount(prev => prev + 1), fixes this because each call receives the true latest pending state rather than the value captured in the closure, so two calls correctly produce two increments.

Related Functions

stateusereduceruseeffect