Deciding where state lives is the core design decision in every React app; get it right and the rest follows.
The React docs describe a design process that scales from a widget to an app:
- Break the UI into a component hierarchy. Draw boxes around parts of the mockup; each box with one responsibility is a component.
- Build a static version that renders from props, with no state.
- Find the minimal state. Ask of each piece of data: is it passed in from a parent? Does it stay the same over time? Can it be computed from other state or props? If any answer is yes, it's not state. In a filterable product table, the product list (a prop) and the filtered list (derived) aren't state; the search text and the "in stock only" checkbox are.
- Decide who owns each piece of state: the closest common parent of all components that use it.
- Add inverse data flow: children change the owner's state by calling callbacks passed down as props.
When two siblings need the same data, lift the state up: remove it from the children, put it in their parent, and pass the value and a setter-like callback down. The children become controlled components, driven by props, which makes them easy to coordinate (only one accordion panel open at a time, say).
Data flows down through props and events flow up through callbacks. That one-way flow is why React apps stay debuggable: for any value on screen, there's exactly one component that owns it.
Avoid duplicating state (storing selectedItem as a copy when selectedId would do) and avoid contradictory state (isSending and isSent both true). If state lives too high, many components re-render needlessly and props get passed through many layers; context and composition help with that later.
Example
import { useState } from "react";
function Panel({ title, isActive, onShow, children }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
</section>
);
}
// State lifted to the common parent: only one panel can be open.
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
Almaty is the largest city in Kazakhstan.
</Panel>
<Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
The name comes from the Kazakh word for apple.
</Panel>
</>
);
}Remember
- State is data that changes, isn't passed in, and can't be computed from anything else
- Put state in the closest common parent of the components that use it
- Data flows down as props; changes flow up through callback props
Watch out for
- Keeping the same data in two components' state and trying to sync them instead of lifting it up.