Skip to content

Lifting State and Component Communication

At the end of this class, you should be able to…

  • Explain the concept of lifting state up and when to apply it in React applications.
  • Pass state and event handlers between parent and child components using props.
  • Identify the challenges of prop drilling and discuss patterns for managing shared state effectively.

Additional Notes:

  • Emphasize the “single source of truth” principle in React state management.
  • Encourage visualizing data flow diagrams before coding to clarify component relationships.
  • Avoid introducing Context API yet - save it for global or complex state lessons (Week 8+).
  • In React 19, use and server-driven state patterns simplify lifting in async data scenarios - mention as a preview only.

This lesson connects three core ideas:

  • Working with forms (submission + simple validation)
  • Conditional rendering
  • Lifting state to share data between components

We will extend the existing Filters, Results, and Details components in the NAIT Student Resources project.

In Lesson 12, we added local state:

  • searchTerm, selectedCategories, and openNowOnly inside Filters
  • selectedResource inside Results

Each component owns its own state.

But now we want:

  • Filters to affect Results
  • Selecting a result to affect Details

To do that, we need shared state.

Ask:

  • Who needs the filter values?
  • Who needs the selected resource?

Currently:

  • Filters owns filter state
  • Results owns selected resource state

However:

  • Results needs filter state
  • Details needs selected resource

That means this state must move up to their nearest common ancestor.

Open App.jsx (or your top-level layout component).

Add shared state:

App.jsx
...
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategories, setSelectedCategories] = useState([]);
const [openNowOnly, setOpenNowOnly] = useState(false);
const [selectedResource, setSelectedResource] = useState(null);
...

The parent now owns all shared state.

Update how Filters is rendered:

App.jsx
<Filters
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
selectedCategories={selectedCategories}
onCategoryToggle={setSelectedCategories}
openNowOnly={openNowOnly}
onOpenNowChange={setOpenNowOnly}
/>

Inside Filters:

  • Remove local useState
  • Use props instead (add the necessary props based on what we passed above)

Example controlled input:

Filters.jsx
<input
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
/>

For category toggling, implement toggle logic in the parent or pass a toggle handler.

The key idea:

Filters no longer owns state. It receives state and notifies the parent.

Phase 4: Update Results to Use Lifted State

Section titled “Phase 4: Update Results to Use Lifted State”

Render Results like this:

App.jsx
<Results
selectedResource={selectedResource}
onSelectResource={setSelectedResource}
searchTerm={searchTerm}
selectedCategories={selectedCategories}
openNowOnly={openNowOnly}
/>

Inside Results:

  • Replace local selected state
  • Use props instead (add the necessary props based on what we passed above)
Results.jsx
...
onClick={() => onSelectResource(resource)}
...

Results now:

  • Receives filter values
  • Receives selected resource
  • Notifies parent when a resource is selected

Update how Details is rendered in the parent:

App.jsx
{selectedResource ? (
<Details resource={selectedResource} />
) : (
<div className="text-sm text-base-content/70">
Select a resource to view details.
</div>
)}

The resource is now passed as a prop. Update the Details component to make use of the prop for rendering.

This demonstrates:

  • Conditional rendering using a ternary
  • Rendering based entirely on state

No manual DOM updates.

Return to the Filters component.

Update the submit handler:

function handleSubmit(e) {
e.preventDefault();
if (!searchTerm.trim() && selectedCategories.length === 0 && !openNowOnly) {
alert('Please select at least one filter option.');
return;
}
console.log('Filters submitted');
}

Attach it:

<form onSubmit={handleSubmit}>

We are still not filtering the data yet.

Discussion: Is there still a need for the “Filter” submit button? All components are reactive to state changes, so filtering happens without submitting the form.

This step reinforces:

  • Forms still use preventDefault()
  • Validation logic is just JavaScript
  • State is accessible through props

Now, App owns:

  • Filter state
  • Selected resource state

Child components:

  • Receive data via props
  • Send events upward via callbacks

Data flows down. Events flow up.

This is lifting state.

  • Shared state lives in the nearest common ancestor
  • Child components should be as stateless as possible
  • Conditional rendering is just rendering based on state
  • Forms behave the same in React, just declaratively
  • Filters no longer owns shared state
  • Results no longer owns selected resource state
  • App coordinates state across components
  • Details renders conditionally
  • Form submission includes validation
  • Move the conditional rendering for no resource selected into Details (i.e. display a “No resource selected” message in the Details component, not in App.jsx).
  • Implement the virtual options filter
  • Implement the filtering of the results using the passed props in Results.
  1. Stage all changes:
Terminal window
git add -A
  1. Commit:
Terminal window
git commit -m 'Lesson 13 - lifting state and shared communication'
  1. Push:
Terminal window
git push origin main