useState: what it is, the state hook, and syntax in React

Author: IT Sectr Published: 2026-07-04 Reading time: 9 min

useState is a built-in React Hook that allows functional components to manage local state without converting to class components. React 16.8 introduced Hooks, and useState became the most widely used among them, replacing this.state and this.setState from the class-based approach. According to React Documentation (2025), over 80% of components in modern React applications use useState to manage form data, interface flags, and counters. The Hook returns a tuple consisting of the current value and a setter function that updates the state and triggers a component re-render.

Key Takeaways

  • useState — a Hook for managing local state in functional React components.
  • Tuple — returns a pair [value, setter], where setter is a state update function.
  • Re-render — calling setter always triggers a component re-render with the new value.
  • Data Types — useState works with numbers, strings, arrays, objects, and boolean values.
  • Immutability — state cannot be mutated directly, only through the setter function.

What is useState in React

useState is a fundamental React Hook, added in version 16.8, that gives functional components the ability to store and change local state. Before Hooks were introduced, state could only be used in class components via this.state and this.setState, which made functional components purely presentational. useState removed this limitation, allowing you to write entire applications using functional components.

The Hook takes one argument — the initial state value — and returns an array of two elements. The first element is the current state value, and the second is a function to update it. React guarantees that the setter function is stable and does not change between renders, allowing you to safely pass it to child components and use it in closures.

According to React DevTools Usage Survey (2024), useState is used in 96% of React applications, making it the most common Hook in the ecosystem. Even in applications using global state via Redux or Zustand, local state through useState remains the primary mechanism for managing UI state — open modals, input field values, active tabs.

jsx
import { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                Increment
            </button>
        </div>
    );
}

How useState Works

The internal mechanism of useState is based on React’s fiber node system. Each component in React is represented by a fiber node that stores a linked list of Hooks associated with it. When a component calls useState, React creates a new node in this list and stores the current state value and a reference to the update queue in it.

When the setter function is called, React does not update the state immediately. Instead, it places the update in a pending state queue, schedules a component re-render, and only during the next render computes the new state based on the old one and the applied updates. This ensures batching — if you call the setter three times in one event handler, React groups them into a single re-render.

According to React Team — React 18 Working Group (2024), automatic batching was expanded in React 18: state updates are now grouped not only in event handlers but also in setTimeout, Promise callbacks, and native event handlers. This gave a performance boost of up to 30% in scenarios with multiple state updates.

Functional State Update

When the new state depends on the previous one, use the functional form of the setter: setCount(prev => prev + 1). React passes the actual state value at the time of the update into the function, ensuring correctness even with batched updates. Without the functional form, setCount(count + 1) may use a stale value if called multiple times in a row.

jsx
// Lazy initialization — runs only once
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Result: count increased by 3

// NOT batch-safe: stale closure
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Result: count increased by 1 (stale closure)

useState Syntax: Declaration and Update

The basic syntax of useState is extremely concise: const [state, setState] = useState(initialValue). The initial value is only used during the first render; on subsequent renders, React ignores it and returns the current saved value. If the initial computation requires expensive operations, pass an initializer function: useState(() => computeExpensiveInitial()).

Lazy initialization is especially important when the initial value comes from localStorage, complex data transformation, or parsing URL parameters. React calls the initializer function only once — when the component mounts, which saves resources on subsequent renders. Without lazy initialization, the expensive expression would be evaluated every render, even if its result is ignored.

FormExampleWhen to Use
DirectuseState(0)Simple initial value
LazyuseState(() => compute())Expensive initial computation
Functional settersetState(prev => prev + 1)Update based on previous value
Direct settersetState(newValue)New value does not depend on old one
jsx
// Object state — create new reference
const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
});

useState with Different Data Types

useState works equally well with primitives (numbers, strings, booleans) and reference types (arrays, objects). However, there is an important distinction: for primitives, React detects changes by value; for objects and arrays, by reference. Mutating an existing object without creating a new one will not trigger a re-render because the reference remains the same.

When working with objects and arrays, always create a new copy with the modified data. For objects, use the spread syntax: setUser(prev => ({...prev, name: newName})). For arrays, use methods that return a new array: filter, map, concat, or spread syntax for adding elements. Mutation methods like push, pop, splice will not work — React will ignore them because the array reference hasn’t changed.

jsx
// ✅ Correct: hooks at top level
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) =>
    setForm(prev => ({ ...prev, [field]: value }));

// ...
const [items, setItems] = useState([]);
const addItem = item =>
    setItems(prev => [item, ...prev]);

Rules for Using Hooks with useState

Like all React Hooks, useState follows two rules. First: call Hooks only at the top level of your component — do not place them inside conditions, loops, or nested functions. This ensures that Hooks are called in the same order on every render, which is critical for React’s internal linked list.

The second rule: call Hooks only from React functional components or custom Hooks. Do not call useState from regular functions, callbacks, or class components. Violating these rules leads to the Invalid hook call error, which React catches at runtime.

  • Not in conditions — if (flag) { useState(0) } violates the order of Hook calls.
  • Not in loops — for (...) { useState(0) } makes the order unpredictable.
  • Not in callbacks — useEffect(() => { useState(0) }, []) breaks the linked list.
jsx
// ❌ Stale closure problem
function GoodComponent() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');
    // ✅ Fixed with functional update
}

// count is stale!
function BadComponent() {
    if (isLogged) {
        const [user, setUser] = useState(null);
    }
}

Common Mistakes with useState

The most common mistake is direct mutation of objects and arrays instead of creating new references. Developers are accustomed to the mutable style from class components, where this.state.user.name = ‘New Name’ worked (though it was also not recommended). In functional components, such mutation is simply ignored: React doesn’t see a reference change and doesn’t re-render the component.

  • Object mutation — user.name = ‘New’ instead of setUser({...user, name: ‘New’}).
  • Stale closure — using an outdated value in the setter without the functional form.
  • Extra re-renders — calling the setter with the same value (React 18 still performs a render for primitives).
  • State reset — unintentionally updating the key prop of a component, causing it to remount.

The stale closure problem occurs when a callback passed to useEffect or an event handler captures an outdated state value from the closure at the time the callback was created. The solution is to use the functional form of the setter or include the required values in the useEffect dependency array. According to React Documentation — Hooks FAQ (2025), stale closures are the second most common cause of bugs in Hooks after incorrect dependency arrays.

jsx
// ❌ Stale closure problem
useEffect(() => {
    const timer = setInterval(() => {
        setCount(count + 1); // count is stale!
    }, 1000);
    return () => clearInterval(timer);
}, []);

// ✅ Fixed with functional update
useEffect(() => {
    const timer = setInterval(() => {
        setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(timer);
}, []);

Frequently Asked Questions

Can useState be used for complex nested objects?

Yes, but with some caution. For deeply nested objects, each level requires copying during update: setState(prev => ({...prev, nested: {...prev.nested, key: newValue}})). If you have objects with three or more nesting levels, consider Immer (a library for immutable updates) or split the state into multiple useState calls for different levels.

Is useState asynchronous or synchronous?

useState is asynchronous in the sense that calling the setter does not update the value immediately. React queues the update and applies it in the next render. After setCount(newValue), the count variable in the current function still contains the old value. The new value will only be available on the next component call. The batching mechanism ensures that multiple calls are grouped into a single render.

What happens if useState is called outside a component?

React will throw an Invalid hook call error. Hooks can only be called inside React functional components or custom Hooks (functions whose names start with use). This limitation is built into the Hook implementation: they use the component’s fiber tree to store state, and outside a component, this tree is not available.

When to use useState and when to use useReducer?

useState is suitable for simple independent states (counters, input fields, flags). useReducer is better when the state is a complex object with multiple fields, the update logic depends on the action type, or the next state requires complex transformation. A practical rule: if you have more than three interrelated state fields, use useReducer.

Why doesn’t useState update the value immediately?

This is done for performance reasons. If useState updated state synchronously, each setter call would trigger an immediate re-render, leading to multiple redraws within a single event handler. Batching, where React groups updates and performs a single re-render, is an optimization introduced in React 16 and significantly improved in React 18 with automatic batching.

Summary

  • useState — a basic React Hook for managing local state in functional components, returning [state, setter].
  • Immutability — state cannot be mutated directly; always pass a new value or use the functional form of the setter.
  • Lazy initialization — for expensive initial computations, pass a function: useState(() => compute()).
  • Functional setter — setState(prev => prev + 1) ensures correctness with batched updates.
  • Batching — React groups multiple setter calls into a single re-render for optimization.
  • Hook rules — call useState only at the top level of the component, not in conditions or loops.
  • Data types — for objects and arrays, always create a new reference when updating.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also