React state is local — it lives in the component that declares it and is invisible to siblings and cousins in the component tree. When two components need to work with the same piece of data — for example, a search bar and the post list that should filter based on the search — both components cannot have their own independent state. The solution is lifting state up: move the shared state to the nearest common ancestor and pass it down via props. This is one of the most fundamental React patterns, and it is how the MERN Blog’s search, filter, and pagination features are coordinated.
The Problem — Sibling Components Need Shared Data
❌ Without lifting state — siblings cannot see each other's state:
BlogPage
├── SearchBar (has its own query state: 'mern')
└── PostList (has its own posts — does NOT know about SearchBar's query)
SearchBar cannot tell PostList what the user typed.
PostList cannot filter based on the search query.
Solution: lift the query state up to BlogPage.
✅ After lifting state — BlogPage owns query, passes it down:
BlogPage (owns: query state, posts state)
├── SearchBar (receives: query, onQueryChange ← passes query up via callback)
└── PostList (receives: filteredPosts ← derived from query + posts in BlogPage)
Lifting State Up — Complete Example
// BEFORE: state trapped in children, no coordination possible
function SearchBar() {
const [query, setQuery] = useState(''); // isolated — PostList cannot see this
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
function PostList() {
const [posts] = useState(SAMPLE_POSTS); // PostList does not know about query
return <ul>{posts.map(p => <li key={p._id}>{p.title}</li>)}</ul>;
}
// AFTER: state lifted to BlogPage, coordinates both children
function BlogPage() {
// ── Lifted state — BlogPage owns what both children need ───────────────────
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('all'); // 'all' | 'published' | 'draft'
const [posts, setPosts] = useState(SAMPLE_POSTS);
// ── Derived values — computed from state, no extra state needed ────────────
const filteredPosts = posts
.filter(p => filter === 'all' || (filter === 'published' ? p.published : !p.published))
.filter(p => p.title.toLowerCase().includes(query.toLowerCase()));
return (
<div className="blog-page">
{/* SearchBar receives query + callback to update it */}
<SearchBar
query={query}
onQueryChange={setQuery}
/>
{/* FilterBar receives filter + callback to update it */}
<FilterBar
activeFilter={filter}
onFilterChange={setFilter}
/>
{/* PostList receives the already-filtered posts */}
<PostList posts={filteredPosts} />
</div>
);
}
// SearchBar: controlled by parent, no internal state for query
function SearchBar({ query, onQueryChange }) {
return (
<input
value={query}
onChange={e => onQueryChange(e.target.value)}
placeholder="Search posts..."
/>
);
}
// FilterBar: controlled by parent, no internal state for filter
function FilterBar({ activeFilter, onFilterChange }) {
const filters = ['all', 'published', 'draft'];
return (
<div className="filter-bar">
{filters.map(f => (
<button
key={f}
onClick={() => onFilterChange(f)}
className={activeFilter === f ? 'btn btn--active' : 'btn'}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
);
}
When to Lift State vs When to Use Local State
| Scenario | Pattern |
|---|---|
| A dropdown’s open/closed state — used only by that dropdown | Local state in the dropdown |
| A form’s field values — used only by that form | Local state in the form |
| A search query that filters a list in a sibling | Lift to common parent |
| Selected item that is displayed in two different sidebar panels | Lift to common parent |
| The current user — needed everywhere in the app | React Context (Chapter 21) |
| Data fetched from the API — needed on one page only | Local state in the page component |
Pagination State — Another Lifting Example
function BlogPage() {
const [posts, setPosts] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(false);
// Fetch posts when page changes
useEffect(() => {
setLoading(true);
fetch(`/api/posts?page=${currentPage}&limit=10`)
.then(r => r.json())
.then(data => {
setPosts(data.data);
setTotalPages(data.pages);
setLoading(false);
});
}, [currentPage]);
return (
<div>
<PostList posts={posts} loading={loading} />
{/* Pagination controls receive state + callback */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
);
}
function Pagination({ currentPage, totalPages, onPageChange }) {
return (
<div className="pagination">
<button
disabled={currentPage === 1}
onClick={() => onPageChange(p => p - 1)}
>← Previous</button>
<span>Page {currentPage} of {totalPages}</span>
<button
disabled={currentPage === totalPages}
onClick={() => onPageChange(p => p + 1)}
>Next →</button>
</div>
);
}
Common Mistakes
Mistake 1 — Duplicating state in both parent and child
❌ Wrong — child has its own copy of state that must be synced with the parent:
function SearchBar({ initialQuery, onSearch }) {
const [query, setQuery] = useState(initialQuery); // copy of parent state
// Now parent's state and child's state can drift out of sync
const handleChange = (e) => {
setQuery(e.target.value);
onSearch(e.target.value); // must remember to call both
};
}
✅ Correct — child reads from prop, parent owns the state:
function SearchBar({ query, onQueryChange }) { // fully controlled — no local state
return <input value={query} onChange={e => onQueryChange(e.target.value)} />;
}
Mistake 2 — Lifting too high unnecessarily
❌ Wrong — a modal’s open/closed state lifted to the App root:
// App.jsx
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
// Passed through 5 intermediate components — prop drilling nightmare
✅ Correct — modal state stays in the component that opens it:
function PostCard({ post }) {
const [showDeleteModal, setShowDeleteModal] = useState(false); // local ✓
...
}
Mistake 3 — Forgetting to reset page when filter changes
❌ Wrong — user is on page 5 of results, changes filter, sees page 5 of the new (shorter) results:
const handleFilterChange = (newFilter) => {
setFilter(newFilter); // changes filter but forgets to reset page
};
✅ Correct — reset dependent state when parent state changes:
const handleFilterChange = (newFilter) => {
setFilter(newFilter);
setCurrentPage(1); // reset to page 1 whenever filter changes ✓
};
Quick Reference
| Pattern | Code |
|---|---|
| Parent owns shared state | const [query, setQuery] = useState('') |
| Pass state down | <SearchBar query={query} onQueryChange={setQuery} /> |
| Child updates via callback | onChange={e => onQueryChange(e.target.value)} |
| Derive filtered data | const filtered = items.filter(i => i.name.includes(query)) |
| Reset dependent state | setFilter(f); setPage(1); |