State Scope

Hold each piece of React state in the lowest common ancestor that consumes it; lift only when multiple siblings need the same data.

Why?  Minimises re‑render surface and keeps ownership obvious.

When you’re new to React it’s tempting to stash all your state in a parent component “just to be safe.” Unfortunately, every time that state changes, everything below it re‑renders—even parts that don’t care. This slows things down, hides the real owner of the data, and makes future refactors painful. By keeping state close to where it’s used, each update only touches the components that actually need to react, which means faster screens and cleaner code.

Correct Example

import { useEffect, useState } from "react";

type Todo = { id: number; text: string };

export default function TodoList() {
  // Server‑fetched data belongs here: multiple items depend on it.
  const [todos, setTodos] = useState<Todo[]>([]);

  useEffect(() => {
    fetch("/api/todos")
      .then((res) => res.json())
      .then(setTodos);
  }, []);

  return (
    <ul className="space-y-2">
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoItem({ todo }: { todo: Todo }) {
  // Checkbox state is local—only this row cares.
  const [checked, setChecked] = useState(false);

  return (
    <li className="flex items-center gap-2">
      <input
        type="checkbox"
        checked={checked}
        onChange={() => setChecked(!checked)}
      />
      <span className={checked ? "line-through" : ""}>{todo.text}</span>
    </li>
  );
}

Incorrect Example

// Hoisting every item’s checkbox state to the top component.
function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [checkedMap, setCheckedMap] = useState<Record<number, boolean>>({});

  useEffect(() => {
    fetch("/api/todos")
      .then((res) => res.json())
      .then(setTodos);
  }, []);

  const toggle = (id: number) =>
    setCheckedMap((prev) => ({ ...prev, [id]: !prev[id] }));

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={!!checkedMap[todo.id]}
            onChange={() => toggle(todo.id)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
// A single checkbox flips? The whole list (and any unrelated siblings)
// still re-renders. Performance tanks and intent is murky.

Key Takeaways

  • Own it where you use it: Let the component that reads and writes the state also own it.
  • Lift sparingly: Only move state up when two or more sibling branches genuinely need the same data or control.
  • Prop‑drilling is fine (within reason): Passing a few props downward beats a global re‑render upward.
  • Performance follows architecture: Local state limits render work and keeps React’s diffing efficient.