HomeAbout Me

Advanced React APIs 4: Shared Context

By Daniel Nguyen
Published in React JS
May 24, 2025
3 min read
Advanced React APIs 4: Shared Context

Shared Context

Let’s say you have the following custom hook:

function useCount() {
const [count, setCount] = useState(0)
const increment = () => setCount((c) => c + 1)
return { count, increment }
}

Then, you have two components, one to display the count and another to increment it:

function DisplayCount() {
const { count } = useCount()
return <div>{count}</div>
}
function IncrementCount() {
const { increment } = useCount()
return <button onClick={increment}>Increment</button>
}
function App() {
return (
<div>
<DisplayCount />
<IncrementCount />
</div>
)
}

If you tried that you’d find that clicking the increment button doesn’t work. That’s because each component is creating its own instance of the state in the useCount hook, so they’re not sharing the same state. In fact, even if you call the hook multiple times in the same component, you’d still get different instances of the state.

Often this is what you want. You want the isolation. But sometimes, you want to have that state be shared between the components.

Often you solve this problem by lifting state, but sometimes that’s not very practical because you’re many layers away from the components that need to share the state or perhaps there’s a router involved and you aren’t the one rendering the components and don’t have an easy way to pass props down.

Another situation where you would use this is if you wanted to implicitly share state between components so consumers of those components can use them without having to know about the state. This is a pattern known as “Compound Components” and we’ll learn more about that in the Compound Components exercise of the Advanced React Patterns workshop.

In these cases, you can use context. React router actually has a built-in feature that allows you to pass state into context and access that value in child routes called outletContext and in my apps this is what I use most of the time. However, sometimes I do still reach for context.

Here’s how you would use context to share state between components:

import { createContext, use } from 'react'
type CountContextType = {
count: number
increment: () => void
}
const CountContext = createContext<CountContextType | null>(null)
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
const increment = () => setCount((c) => c + 1)
const value = { count, increment }
return <CountContext value={value}>{children}</CountContext>
}
function useCount() {
const context = use(CountContext)
if (!context) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function DisplayCount() {
const { count } = useCount()
return <div>{count}</div>
}
function IncrementCount() {
const { increment } = useCount()
return <button onClick={increment}>Increment</button>
}
function App() {
return (
<CountProvider>
<div>
<DisplayCount />
<IncrementCount />
</div>
</CountProvider>
)
}

The CountProvider component is a context provider. It’s a component that handles the state and provides it to the components that need it. The useCount hook is a custom hook that uses the use hook to access the context. The DisplayCount and IncrementCount components are the consumers of the context.

Whenever the value prop of the context provider changes, all the components that consume the context value will rerender to get those updates. And they will all be referencing the same state.

Providers can be nested and the closest provider to the consuming component will be the one that provides the value. If there is no provider, the default value will be used. In this case, the default value is null and we throw an error if the hook is used without a provider. This is generally a good practice, but there are use cases where you might want to provide a default value. You’ll know when you need to do that, but most of the time you’ll want to require a context provider.

Context Provider

🧝‍♂️ I was

refactoring things
a bit and decided we shouldn’t need to pass the query and setSearchParams as props. Instead I just wanted to call useSearchParams in each of the components that need them (there’s only one URL afterall).

Well that didn’t work. Turns out we now have multiple instances of the search params state and multiple subscriptions to the popstate event. That’s not what we want at all. It’s busted right now. Can you fix this with context?

👨‍💼 No worries Kellie. We can fix this.

So we want to create a context provider that will provide the useSearchParams hook to the rest of the app.

We’re going to take this in steps though. First we’ll create the context provider, render it around the app, and then make a new useSearchParams hook that will use the context value.

Good luck! When you’re finished, the app should be working again!

Context Hook

🧝‍♂️ I was playing around with the app and I realized that we have a default value for our context provider. So I thought maybe we could

remove the context
provider. Unfortunately that didn’t work. Do you know why?

👨‍💼 Yeah, it’s because even though the searchParams are shared, they’re not updated when calling setSearchParams. This is because the searchParams object is no longer state. It’s just a plain object. So when we call setSearchParams, nothing rerenders (and even if it did, it wouldn’t have the updated searchParams).

So having a default in our context is pretty meaningless. So why don’t we default it to null and then in our useSearchParams() hook we can throw an error if the context is null? Go ahead and give it a try.

When you’re done, the app should be working again.

import { createContext, useEffect, useState, use, useCallback } from 'react'
import * as ReactDOM from 'react-dom/client'
import {
type BlogPost,
generateGradient,
getMatchingPosts,
} from '#shared/blog-posts'
import { setGlobalSearchParams } from '#shared/utils'
type SearchParamsTuple = readonly [
URLSearchParams,
typeof setGlobalSearchParams,
]
const SearchParamsContext = createContext<SearchParamsTuple | null>(null)
function SearchParamsProvider({ children }: { children: React.ReactNode }) {
const [searchParams, setSearchParamsState] = useState(
() => new URLSearchParams(window.location.search),
)
useEffect(() => {
function updateSearchParams() {
setSearchParamsState((prevParams) => {
const newParams = new URLSearchParams(window.location.search)
return prevParams.toString() === newParams.toString()
? prevParams
: newParams
})
}
window.addEventListener('popstate', updateSearchParams)
return () => window.removeEventListener('popstate', updateSearchParams)
}, [])
const setSearchParams = useCallback(
(...args: Parameters<typeof setGlobalSearchParams>) => {
const searchParams = setGlobalSearchParams(...args)
setSearchParamsState((prevParams) => {
return prevParams.toString() === searchParams.toString()
? prevParams
: searchParams
})
return searchParams
},
[],
)
const searchParamsTuple = [searchParams, setSearchParams] as const
return (
<SearchParamsContext value={searchParamsTuple}>
{children}
</SearchParamsContext>
)
}
export function useSearchParams() {
const context = use(SearchParamsContext)
if (!context) {
throw new Error(
'useSearchParams must be used within a SearchParamsProvider',
)
}
return context
}
const getQueryParam = (params: URLSearchParams) => params.get('query') ?? ''
function App() {
return (
<SearchParamsProvider>
<div className="app">
<Form />
<MatchingPosts />
</div>
</SearchParamsProvider>
)
}
function Form() {
const [searchParams, setSearchParams] = useSearchParams()
const query = getQueryParam(searchParams)
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)
setSearchParams(
{ query: newWords.filter(Boolean).join(' ').trim() },
{ replace: true },
)
}
return (
<form onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="searchInput">Search:</label>
<input
id="searchInput"
name="query"
type="search"
value={query}
onChange={(e) =>
setSearchParams({ query: e.currentTarget.value }, { replace: true })
}
/>
</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>
</form>
)
}
function MatchingPosts() {
const [searchParams] = useSearchParams()
const query = getQueryParam(searchParams)
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)
ReactDOM.createRoot(rootEl).render(<App />)
html,
body {
margin: 0;
}
.app {
margin: 40px auto;
max-width: 1024px;
form {
text-align: center;
}
}
.post-list {
list-style: none;
padding: 0;
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
li {
position: relative;
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid #ddd;
width: 320px;
transition: transform 0.2s ease-in-out;
a {
text-decoration: none;
color: unset;
}
&:hover,
&:has(*:focus),
&:has(*:active) {
transform: translate(0px, -6px);
}
.post-image {
display: block;
width: 100%;
height: 200px;
}
button {
position: absolute;
font-size: 1.5rem;
top: 20px;
right: 20px;
background: transparent;
border: none;
outline: none;
&:hover,
&:focus,
&:active {
animation: pulse 1.5s infinite;
}
}
a {
padding: 10px 10px;
display: flex;
gap: 8px;
flex-direction: column;
h2 {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
}
p {
margin: 0;
font-size: 1rem;
color: #666;
}
}
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}

Tags

#React

Share

Previous Article
Advanced React APIs 3: Custom Hooks

Table Of Contents

1
Shared Context
2
Context Provider
3
Context Hook

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media