Designing State in React

Good state design gives every changing value one clear owner. Store the smallest source of truth, calculate what follows from it, and pass data down to the components that render it.

Start with the source of truth

Before adding useState, list the pieces of data the interface needs. Then separate values the app must remember from values it can calculate. This prevents two state variables from disagreeing.

Store

Keep information that changes and cannot be calculated from current props or state.

Derive

Calculate values that follow from the source of truth during rendering.

Share

Move state to the closest common parent when multiple children need to coordinate.

Choose minimal state

State is the information React must remember between renders. More state is not more capable. Every extra state variable adds another value that code must initialize, update, and keep consistent.

  1. Does the value remain constant?

    If so, declare it outside the component or as a regular constant, not state.

  2. Does a parent already provide it?

    Read the prop directly. Copying a prop into state creates a second source of truth unless the copy intentionally represents a separate value, such as an editable draft.

  3. Can current props or state calculate it?

    If so, derive it during render instead of synchronizing another state variable.

  4. Must React remember it after an interaction?

    If it changes over time and the earlier questions did not remove it, it is a good state candidate.

A product search only stores the query. The matching list and result count follow from that one value.
import { useState } from "react";

function ProductSearch({ products }) {
  const [query, setQuery] = useState("");

  // Derived during this render, so it always matches query and products.
  const visibleProducts = products.filter(product =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );
  const resultCount = visibleProducts.length;

  return (
    <section>
      <input
        value={query}
        onChange={event => setQuery(event.target.value)}
        placeholder="Search products"
      />
      <p>{resultCount} results</p>
      <ul>
        {visibleProducts.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </section>
  );
}

The source of truth is small: the parent owns products, and this component owns query. It does not need visibleProducts or resultCount in state.

Calculate derived values during render

A derived value is a calculation based entirely on current props or state. Regular JavaScript calculations inside the component run whenever it renders, so the result automatically describes the same snapshot as the JSX.

Redundant state

Storing fullName requires extra updates and can briefly leave the interface showing an old value.

Derived value

Calculating fullName makes it correct whenever either source value changes.

Do not use an Effect and a third state variable for a value that concatenation can produce.
function NameEditor() {
  const [firstName, setFirstName] = useState("Asha");
  const [lastName, setLastName] = useState("Rao");

  // This is current on every render. No fullName state is needed.
  const fullName = `${firstName} ${lastName}`;

  return (
    <form>
      <input
        value={firstName}
        onChange={event => setFirstName(event.target.value)}
      />
      <input
        value={lastName}
        onChange={event => setLastName(event.target.value)}
      />
      <p>Welcome, {fullName}</p>
    </form>
  );
}

Deriving a value does not mean calculating it only once. It means calculating it from the current render's inputs. If a calculation is measurably expensive, useMemo can cache it as a performance optimization, but it should not be needed for correctness.

Lift state when components must agree

Two sibling components cannot directly share local state. Move their shared state to their closest common parent. The parent passes each child the current value and an event handler that lets the child request a change.

The parent owns which panel is active, so opening one panel can close the other.
function Accordion() {
  const [activeId, setActiveId] = useState(1);

  return (
    <>
      <Panel
        title="About"
        isOpen={activeId === 1}
        onOpen={() => setActiveId(1)}
      >
        React builds interfaces from components.
      </Panel>
      <Panel
        title="Details"
        isOpen={activeId === 2}
        onOpen={() => setActiveId(2)}
      >
        The parent coordinates both panels.
      </Panel>
    </>
  );
}

function Panel({ title, isOpen, onOpen, children }) {
  return (
    <section>
      <h3>{title}</h3>
      {isOpen ? children : <button onClick={onOpen}>Open</button>}
    </section>
  );
}

Lift state only as high as coordination requires. Keeping unrelated state near the component that uses it makes ownership easier to understand and avoids rerendering a larger subtree without a reason.

One-way data flow

React data flows from parent to child through props. A child does not modify those props. It reports an interaction by calling a handler prop, the owner updates state, and the new value flows down in the next render.

  1. Value goes downThe parent passes isOpen to a child.
  2. Event goes upThe child calls onOpen after a click.
  3. New value goes downThe parent updates state and renders fresh props.

“Events go up” is a useful shorthand, but React does not automatically send a custom event to the parent. The parent explicitly passes a function, and the child explicitly calls it.

Controlled components

A component is controlled when important information about its behavior comes from props. Its parent owns that state. The child renders the value and uses callback props to request changes.

This input is controlled because React state supplies its value on every render.
function SearchPage() {
  const [query, setQuery] = useState("");

  return (
    <SearchInput
      value={query}
      onChange={setQuery}
    />
  );
}

function SearchInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={event => onChange(event.target.value)}
    />
  );
}
  • Controlled means the parent decides.

    For a custom component, a prop such as isOpen controls behavior. For a form input, value or checked makes React state the source of truth.

  • Uncontrolled means the component keeps local state.

    A custom component may initialize and manage its own state. A DOM input using defaultValue keeps its current value in the DOM.

  • Choose based on coordination.

    Controlled components are easier for a parent to synchronize, validate, or reset. Uncontrolled components can need less wiring when no other component cares about their state.

Do not pass value without a matching onChange that updates it unless the field is intentionally read-only. Otherwise, the input appears frozen because every render restores the same value.

A practical state-design routine

  1. Identify the changing information.

    Ignore values that never change and values already supplied as props.

  2. Remove anything derivable.

    Keep one source of truth and calculate labels, filtered collections, totals, and booleans from it.

  3. Choose the owner.

    Keep state local unless multiple components need the same value, then lift it to their closest common parent.

  4. Connect the flow.

    Pass current values down as props and pass handlers down so children can request updates.

Check your understanding

  1. A component stores items, filter, and visibleItems. Which state is probably redundant?
    Show answer

    visibleItems is derived from items and filter, so calculate it during render. Storing it creates another value that can fall out of sync.

  2. Two sibling panels must never be open at the same time. Where should their open state live?
    Show answer

    Put one shared value, such as activePanelId, in their closest common parent. Pass each panel its open status and an event handler.

  3. What makes an input controlled?
    Show answer

    React supplies its current value or checked prop, and an event handler updates the state that supplies that prop.