HomeAbout Me

React Hooks 3: Lifting State

By Daniel Nguyen
Published in React JS
May 13, 2025
2 min read
React Hooks 3: Lifting State

Lifting State

A common question from React beginners is how to share state between two sibling components. The answer is to “lift the state” which basically amounts to finding the lowest common parent shared between the two components and placing the state management there, and then passing the state and a mechanism for updating that state down into the components that need it.

Let’s look at a simple example.

import { useState } from 'react'
function App() {
return (
<div>
<Counter />
</div>
)
}
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}

In this example, the Counter component has its own state. What if I wanted to make a fancy CountDisplay component that was a sibling to the Counter:

function App() {
return (
<div>
<Counter />
<CountDisplay />
</div>
)
}
function CountDisplay() {
return <div>Count: 0</div>
}

How can I get the CountDisplay to show the current count from the Counter? The answer is to lift the state up to the App component and then pass the state and a mechanism for updating that state down into the Counter and CountDisplay components.

function App() {
const [count, setCount] = useState(0)
return (
<div>
<Counter count={count} setCount={setCount} />
<CountDisplay count={count} />
</div>
)
}
function Counter({ count, setCount }) {
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
function CountDisplay({ count }) {
return <div>Count: {count}</div>
}

Now the App component is managing the state and passing it down to the Counter and CountDisplay components. This is a simple example, but the principle is the same for more complex state management.

There are other ways to get the state around to components that need it. We’ll cover my preferred pattern of using composition in the Advanced React Patterns workshop, but moving state up the tree is pretty common.

Colocating state

One thing that’s not as common as it should be in React is moving state the other direction when changes are made. This is because you don’t have to do it for things to work, but it’s better for performance and maintainability if you do.

Let’s take our example further by saying that we no longer need the CountDisplay component to show the count. Instead, we want to show the count in the button of the Counter. So we might do this:

function App() {
const [count, setCount] = useState(0)
return (
<div>
<Counter count={count} setCount={setCount} />
</div>
)
}
function Counter({ count, setCount }) {
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment ({count})</button>
</div>
)
}

But now the App component is managing the state and passing it down to the Counter component, but nothing but the Counter needs this state. So we can move the state back down to the Counter component:

function App() {
return (
<div>
<Counter />
</div>
)
}
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment ({count})</button>
</div>
)
}

Novice React developers learn to lift state pretty early, but experienced React developers know how to recognize when state can be “pushed down.”

Let’s do both in this exercise.

Read more about state colocation from State Colocation will make your React app faster.

Colocate State

👨‍💼 Well, the users thought they wanted the articles sorted by whether they were favorited… But after using the feature, they found it to be jarring and confusing. So we’re going to need you to remove that feature. Kellie already removed the sorting for you, but she didn’t have time to move the state back to the Card component.

As a community we’re pretty good at lifting state. It becomes natural over time. In fact it’s required to make the feature work. But as you notice here, the functionality we want is already “working” without moving any of the state around so it’s easy to forget to improve the performance and maintainability of our code by moving state back down (or colocate state).

So your job is to move the favorited state back to the Card component.

When you’re finished, the functionality should be no different, but the code should feel simpler.

import { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import {
type BlogPost,
generateGradient,
getMatchingPosts,
} from '#shared/blog-posts'
import { setGlobalSearchParams } from '#shared/utils'
function getQueryParam() {
const params = new URLSearchParams(window.location.search)
return params.get('query') ?? ''
}
function App() {
const [query, setQuery] = useState(getQueryParam)
useEffect(() => {
const updateQuery = () => setQuery(getQueryParam())
window.addEventListener('popstate', updateQuery)
return () => {
window.removeEventListener('popstate', updateQuery)
}
}, [])
return (
<div className="app">
<Form query={query} setQuery={setQuery} />
<MatchingPosts query={query} />
</div>
)
}
function Form({
query,
setQuery,
}: {
query: string
setQuery: (query: string) => void
}) {
const words = query.split(' ').map(w => w.trim())
const dogChecked = words.includes('dog')
const catChecked = words.includes('cat')
const caterpillarChecked = words.includes('caterpillar')
function handleCheck(tag: string, checked: boolean) {
const newWords = checked ? [...words, tag] : words.filter(w => w !== tag)
setQuery(newWords.filter(Boolean).join(' ').trim())
}
return (
<form
action={() => {
setGlobalSearchParams({ query })
}}
>
<div>
<label htmlFor="searchInput">Search:</label>
<input
id="searchInput"
name="query"
type="search"
value={query}
onChange={e => setQuery(e.currentTarget.value)}
/>
</div>
<div>
<label>
<input
type="checkbox"
checked={dogChecked}
onChange={e => handleCheck('dog', e.currentTarget.checked)}
/>{' '}
🐶 dog
</label>
<label>
<input
type="checkbox"
checked={catChecked}
onChange={e => handleCheck('cat', e.currentTarget.checked)}
/>{' '}
🐱 cat
</label>
<label>
<input
type="checkbox"
checked={caterpillarChecked}
onChange={e => handleCheck('caterpillar', e.currentTarget.checked)}
/>{' '}
🐛 caterpillar
</label>
</div>
<button type="submit">Submit</button>
</form>
)
}
function MatchingPosts({ query }: { query: string }) {
const matchingPosts = getMatchingPosts(query)
return (
<ul className="post-list">
{matchingPosts.map(post => (
<Card key={post.id} post={post} />
))}
</ul>
)
}
function Card({ post }: { post: BlogPost }) {
const [isFavorited, setIsFavorited] = useState(false)
return (
<li>
{isFavorited ? (
<button
aria-label="Remove favorite"
onClick={() => setIsFavorited(false)}
>
❤️
</button>
) : (
<button aria-label="Add favorite" onClick={() => setIsFavorited(true)}>
🤍
</button>
)}
<div
className="post-image"
style={{ background: generateGradient(post.id) }}
/>
<a
href={post.id}
onClick={event => {
event.preventDefault()
alert(`Great! Let's go to ${post.id}!`)
}}
>
<h2>{post.title}</h2>
<p>{post.description}</p>
</a>
</li>
)
}
const rootEl = document.createElement('div')
document.body.append(rootEl)
createRoot(rootEl).render(<App />)

Tags

#ReactHooks

Share

Previous Article
React Hooks 2: Side-Effects

Table Of Contents

1
Lifting State
2
Colocate State

Related Posts

React Hook Section 6: Tic Tac Toe
May 16, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media