Lifting State Up — Sharing State Between Components

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)
Note: “Lifting state up” means moving state to the nearest common ancestor of the components that need it — no higher. If only two sibling components need the data, it goes to their direct parent. If components from different branches of the tree need it, it goes to their nearest shared ancestor. Lifting too high (e.g. into the root App component) when only two nearby components need it creates unnecessary re-renders across the whole tree.
Tip: When you find yourself wanting to “send data from a child to a sibling,” that is a signal to lift state up. The child does not send data sideways — it calls a callback (passed from the parent as a prop) which updates the parent’s state, which then flows down as a new prop to the sibling. Data always flows down; events flow up.
Warning: Lifting state too high can cause unnecessary re-renders. When the lifted state changes, the parent and all its children re-render. If you lift state all the way to App, every component in the tree re-renders on every change. Use React Context (Chapter 21) or a state management library when many distant components need to share state — lifting state is the right pattern for closely related components.

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);

🧠 Test Yourself

A SearchBar component has its own query state. A sibling PostList component needs to filter posts based on the query. What is the correct solution?