State ⚡

1. Introduction 👋

State is what makes React applications interactive. While props let components receive data from their parents, state lets a component remember information and update its own UI over time — in response to user input, network responses, or timers.

Information

This tutorial assumes familiarity with Components and JSX, covered in earlier tutorials.

2. What is State? 🤔

State is data that a component owns and can change over time. When state changes, React automatically re-renders the component to reflect the new data on screen.

Code Snippet

import { useState } from 'react';

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}
>>"State is like a component's memory. It lets a component keep track of information and change how it looks over time." — React Documentation

3. Why State? 💡

Without state, components could only render static output based on their props. State enables:

  • Interactivity: Responding to clicks, input, and other user actions.
  • Dynamic UI: Reflecting real-time data like form inputs or loading indicators.
  • Persistence Across Renders: Remembering values between re-renders, unlike regular variables.
  • Encapsulation: Each component instance manages its own independent state.

4. State vs Props âš”ī¸

AspectStateProps
OwnerOwned by the component itselfPassed in from a parent component
MutabilityCan change via setter functionsRead-only from the child's perspective
PurposeTracks internal, changing dataConfigures a component from outside
Triggers Re-renderYes, when updatedYes, when the parent passes new values

5. Understanding useState đŸĒ

useState is the primary Hook used to add state to function components. It returns an array containing the current value and a setter function.

Code Snippet

const [state, setState] = useState(initialValue);
  1. state — the current value, available on every render.
  2. setState — a function used to update the value and trigger a re-render.
  3. initialValue — the value used only on the first render.

6. Creating State 🌱

Code Snippet

import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  return (
    <form>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
    </form>
  );
}

Tip

Call useState once for each independent piece of state, rather than combining unrelated values into a single object.

7. Reading State 👀

The current state value is simply the first element returned by useState. It behaves like a regular variable during render.

Code Snippet

function Profile() {
  const [name, setName] = useState("Guest");

  return <h1>Welcome, {name}!</h1>;
}

Note

State is only guaranteed to reflect the latest value after a re-render — reading it immediately after calling the setter within the same function still returns the old value.

8. Updating State âœī¸

Calling the setter function schedules a state update and triggers a re-render of the component (and its children, where applicable).

Code Snippet

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? "ON ✅" : "OFF ⛔"}
    </button>
  );
}

Important

Never mutate state directly (e.g., state.value = 5). Always use the setter function so React knows to re-render.

9. Multiple State Variables đŸ”ĸ

Components commonly use several independent useState calls to track distinct pieces of data separately.

Code Snippet

function SignupForm() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [agreedToTerms, setAgreedToTerms] = useState(false);

  // Each variable updates independently
}

Best Practice

Group state together in an object only when the values are always updated together; otherwise, prefer separate useState calls for clarity.

10. Functional State Updates 🔁

When new state depends on the previous state, pass a function to the setter instead of a direct value. This guarantees you're working with the most up-to-date state, especially important with rapid or batched updates.

Code Snippet

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

  function incrementTwice() {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    // Correctly results in +2
  }

  return <button onClick={incrementTwice}>Count: {count}</button>;
}

Warning

Using setCount(count + 1) twice in a row only increments by 1 total, since both calls capture the same stale count value from the current render.

11. Initial State 🌟

The value passed to useState is used only once, during the component's first render. On subsequent renders, React ignores this argument and uses the current stored state instead.

Code Snippet

function Timer() {
  const [seconds, setSeconds] = useState(0); // used only on first render
  // ...
}

12. Lazy Initialization 💤

If computing the initial state is expensive, pass a function to useState instead of a value. React calls this function only once, on the first render.

Code Snippet

function TodoList() {
  const [todos, setTodos] = useState(() => {
    // Expensive operation runs only once
    return loadTodosFromLocalStorage();
  });
}

Caution

Writing useState(loadTodosFromLocalStorage()) instead of useState(() => loadTodosFromLocalStorage()) runs the expensive function on every render, even though the result is discarded after the first.

13. Updating Objects in State 🧱

Because state must be treated as immutable, updating an object requires creating a new object rather than mutating the existing one.

Code Snippet

function ProfileForm() {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  function updateAge() {
    setUser({ ...user, age: user.age + 1 });
  }

  return <button onClick={updateAge}>Birthday 🎂</button>;
}

Danger

Writing user.age = user.age + 1 directly mutates the object without triggering a re-render, since React compares object references, not deep contents.

14. Updating Arrays in State 📚

Similarly, arrays in state must be updated immutably, using methods that return new arrays instead of mutating methods.

Code Snippet

function TodoList() {
  const [todos, setTodos] = useState(["Learn React"]);

  function addTodo(newTodo) {
    setTodos([...todos, newTodo]); // ✅ new array
  }

  function removeTodo(index) {
    setTodos(todos.filter((_, i) => i !== index)); // ✅ new array
  }
}
Avoid (Mutating)Prefer (Immutable)
array.push(item)[...array, item]
array.pop()array.slice(0, -1)
array.splice(i, 1)array.filter((_, idx) => idx !== i)
array[i] = newValarray.map((v, idx) => idx === i ? newVal : v)

15. Nested State đŸĒ†

Updating deeply nested state requires copying every level of the structure that changes, since immutability applies at each nested layer.

Code Snippet

function AddressForm() {
  const [user, setUser] = useState({
    name: "Alice",
    address: { city: "Chennai", zip: "600001" },
  });

  function updateCity(newCity) {
    setUser({
      ...user,
      address: { ...user.address, city: newCity },
    });
  }
}

Tip

For deeply nested state, consider using a library like immer to simplify updates with mutable-looking syntax that produces immutable results under the hood.

16. Immutable State Updates 🔒

Immutability is a core rule of React state: never modify existing state objects or arrays directly. Always create a new copy with the desired changes.

  • Enables React to detect changes efficiently via reference comparison.
  • Prevents subtle, hard-to-trace bugs from unexpected mutations.
  • Supports predictable debugging and features like time-travel debugging.

Best Practice

Use the spread operator (...) and array/object methods that return new copies rather than mutating methods.

17. State Batching đŸŽ¯

React batches multiple state updates that occur within the same event handler into a single re-render, improving performance by avoiding redundant renders.

Code Snippet

function Form() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(count + 1);
    setFlag(true);
    // Only ONE re-render happens, not two
  }
}

Information

Since React 18, batching applies automatically even inside promises, timeouts, and native event handlers — not just React's synthetic events.

18. State Queue đŸ“Ĩ

When multiple state updates are triggered before a re-render, React processes them as a queue. Functional updates ensure each update in the queue receives the correctly updated previous value.

Code Snippet

function QueueExample() {
  const [number, setNumber] = useState(0);

  function handleClick() {
    setNumber((n) => n + 1); // queued: 0 -> 1
    setNumber((n) => n + 1); // queued: 1 -> 2
    setNumber((n) => n + 1); // queued: 2 -> 3
  }

  return <button onClick={handleClick}>{number}</button>; // results in 3
}

19. Asynchronous State Updates âąī¸

State updates are not applied immediately — React schedules them and applies the change before the next render, meaning the updated value isn't accessible right after calling the setter.

Code Snippet

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

  function handleClick() {
    setCount(count + 1);
    console.log(count); // logs the OLD value, not the new one
  }
}

Caution

To perform logic after a state update is applied, use a useEffect hook that depends on that state, rather than reading the variable immediately after the setter call.

20. Derived State 🧮

Not every value needs its own useState. If a value can be calculated from existing state or props during render, it's usually better to derive it directly instead of storing it redundantly.

Code Snippet

function Cart({ items }) {
  // ❌ Avoid: redundant state that can go out of sync
  // const [total, setTotal] = useState(0);

  // ✅ Prefer: derived directly during render
  const total = items.reduce((sum, item) => sum + item.price, 0);

  return <p>Total: ${total.toFixed(2)}</p>;
}

Best Practice

If a value can be computed from props or existing state, don't duplicate it in its own useState — derive it during render instead.

21. Lifting State Up âŦ†ī¸

When multiple components need to share the same state, move that state to their closest common parent, then pass it down via props.

Code Snippet

function Parent() {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <div>
      <ItemList selectedId={selectedId} onSelect={setSelectedId} />
      <ItemDetails selectedId={selectedId} />
    </div>
  );
}

Tip

Lifting state up is one of the most common React patterns — it keeps sibling components in sync without duplicating state.

22. Sharing State 🔗

Beyond lifting state to a shared parent, larger applications may share state across distant components using the Context API or dedicated state management libraries.

ScopeRecommended Approach
Sibling components, shared parentLift state up
Deeply nested componentsContext API
Complex, app-wide stateState management library (Zustand, Redux)

Reference

Context and global state management are covered in depth in their own dedicated tutorials.

23. Preserving State 💾

React preserves a component's state across re-renders as long as it remains at the same position in the component tree, with the same type.

Code Snippet

function App({ showCounter }) {
  return (
    <div>
      {showCounter && <Counter />}
      {/* Counter's state resets if unmounted and remounted */}
    </div>
  );
}

Note

State is tied to a component's position and identity in the tree, not the component definition itself — moving a component to a different position resets its state.

24. Resetting State 🔄

To intentionally reset a component's state (e.g., when switching between items), change its key prop — React treats a new key as an entirely new component instance.

Code Snippet

function ProfileEditor({ userId }) {
  return <Form key={userId} />;
  // Changing userId forces Form to fully reset its internal state
}

Tip

The key trick is a common pattern for resetting form state, animations, or any local state tied to a specific piece of data.

25. State Management Patterns đŸ›ī¸

Managed entirely within a single component using useState. Best for UI-only concerns like toggles, form inputs, or hover states.

Shared between a few closely related components by lifting state to their common parent. Best for moderate sharing needs.

Managed via Context or libraries like Redux or Zustand for state needed across many, unrelated parts of the app.

26. Common State Problems 🐛

  • Stale closures: Reading state variables captured from an old render inside callbacks or effects.
  • Direct mutation: Modifying objects/arrays in place instead of creating new copies.
  • Too much state: Storing values that could instead be derived during render.
  • State duplicated across components: Leading to components falling out of sync.
  • Unnecessary re-renders: Storing frequently changing values at too high a level in the tree.

27. State Best Practices 🌟

  1. Keep state as minimal as possible — derive values instead of storing redundant copies.
  2. Use functional updates when new state depends on the previous value.
  3. Always update objects and arrays immutably.
  4. Lift state up only as far as necessary — avoid placing state higher than needed.
  5. Use the key prop deliberately to reset state when appropriate.
  6. Split unrelated state into separate useState calls for clarity.

28. TypeScript with State 🔷

TS can infer state types automatically from the initial value, or you can specify them explicitly using generics for more complex cases.

Code Snippet

// Inferred type: boolean
const [isVisible, setIsVisible] = useState(false);

// Explicit type for complex state
interface User {
  name: string;
  age: number;
}

const [user, setUser] = useState<User | null>(null);

Tip

Explicitly typing state as <T | null> is a common pattern when the initial value is null before data has loaded.

29. Frequently Asked Questions ❓

Question

Why doesn't my state update immediately after calling the setter?

Answer

State updates are asynchronous and scheduled for the next render. The variable retains its old value until the component re-renders.

Question

Can I use multiple useState calls in one component?

Answer

Yes — using several independent useState calls for unrelated values is a common and recommended pattern.

Question

When should I lift state up versus use Context?

Answer

Lift state up for a small number of closely related components; reach for Context or a state library when state needs to reach many, distant components.

30. Summary 📝

State gives React components memory, enabling dynamic, interactive user interfaces. Mastering useState — including immutable updates, functional updates, and patterns like lifting state up — is essential for building predictable and maintainable applications.

Summary

With state under your belt, the next natural step is exploring Props in depth and the Component Lifecycle through useEffect, which govern how components respond to changes over time.