State lives in a component and is only directly accessible by that component and components it renders (via props). When two sibling components need to share state — a search input and the list it filters, a selected tag and the posts filtered by it — the state must be lifted to their nearest common ancestor. The ancestor owns the state, and the siblings receive the current value and an update callback as props. This “lifting state up” pattern is React’s fundamental mechanism for sharing state without a global store.
Lifting State — Tag Filter Example
// ── BEFORE lifting — each component manages its own state, no sharing ────────
// ❌ Wrong — each TagBadge controls its own selection, no way to
// tell PostList which tag is selected
function TagBadge({ tag }) {
const [isSelected, setIsSelected] = useState(false); // local state
return (
<button onClick={() => setIsSelected(!isSelected)}>
{tag.name}
</button>
);
}
// ── AFTER lifting — ancestor owns state, children receive props ──────────────
// ✓ PostFeed lifts state and coordinates children
function PostFeed({ posts, allTags }) {
// State lifted to common ancestor
const [selectedTag, setSelectedTag] = useState(null);
const filtered = selectedTag
? posts.filter((p) => p.tags.some((t) => t.slug === selectedTag))
: posts;
return (
<div>
{/* TagFilter receives state as prop and setter as callback */}
<TagFilter
tags={allTags}
selected={selectedTag}
onSelect={setSelectedTag}
/>
{/* PostList receives derived filtered data */}
<PostList posts={filtered} />
</div>
);
}
function TagFilter({ tags, selected, onSelect }) {
return (
<div className="flex flex-wrap gap-2 mb-4">
<button
onClick={() => onSelect(null)}
className={selected === null ? "font-bold" : "text-gray-500"}
>
All
</button>
{tags.map((tag) => (
<button
key={tag.id}
onClick={() => onSelect(tag.slug)}
className={selected === tag.slug ? "font-bold text-blue-600" : "text-gray-500"}
>
{tag.name}
</button>
))}
</div>
);
}
selected={selectedTag} onSelect={setSelectedTag}. The child component calls onSelect(newValue), which calls setSelectedTag(newValue) in the parent, triggering a re-render of the parent and flowing the new selected prop back down to the child. The data always flows in one direction: down via props, up via callbacks.LikeButton State Lifting
// ── Lifted: PostCard owns like state for the post ────────────────────────────
function PostCard({ post }) {
const [likeCount, setLikeCount] = useState(post.like_count ?? 0);
const [hasLiked, setHasLiked] = useState(false);
function handleLike() {
if (hasLiked) {
setLikeCount((c) => c - 1);
setHasLiked(false);
} else {
setLikeCount((c) => c + 1);
setHasLiked(true);
}
}
return (
<article>
<h2>{post.title}</h2>
{/* LikeButton receives state + callbacks via props */}
<LikeButton
count={likeCount}
liked={hasLiked}
onLike={handleLike}
/>
</article>
);
}
function LikeButton({ count, liked, onLike }) {
return (
<button
onClick={onLike}
className={liked ? "text-red-500" : "text-gray-400"}
>
{liked ? "♥" : "♡"} {count}
</button>
);
// No local state — pure presenter, all data from props
}
Common Mistakes
Mistake 1 — Lifting too high (every input triggers page-level re-render)
❌ Wrong — search state at app root re-renders entire tree on every keystroke:
function App() {
const [search, setSearch] = useState(""); // too high!
// Every keystroke re-renders the entire application
}
✅ Correct — lift only as high as needed; keep state local to the feature.
Mistake 2 — Duplicating state that already exists in an ancestor
❌ Wrong — child duplicates parent’s state:
function PostCard({ post }) {
const [title, setTitle] = useState(post.title); // copies parent data
// Now you have two sources of truth — which one is correct?
}
✅ Correct — read directly from props; do not copy props into state unless you need independent local modifications.
Quick Reference
| Pattern | Code |
|---|---|
| Lift state | Move useState to nearest common ancestor |
| Pass value down | <Child value={state} /> |
| Pass setter down | <Child onUpdate={setState} /> |
| Child updates parent | props.onUpdate(newValue) |
| Derived state (avoid) | Compute from lifted state instead of useState |