React Context API

Jeff P
9 min readJan 29, 2024

--

The solution to “prop drilling”

The context api allows us to “broadcast” props to the entire app, without drilling through components.

Provider — The provider is what gives all child components access to a value.

Value — this is what we want to share (usually state or functions)

Consumer — the component that needs the value

When the provider value changes, all of the consumers of that contewxt will also re-render.

Using context

The first step is to create a new context. We do this by importing “createContext” from React, in the same way that we do for useState, useEffect, etc.

import { useEffect, useState, createContext } from "react";

We then create a context component. So for example, if we wanted our Post component to consume values from our provider, then we might create a new context component called PostContext…

const PostContext = createContext();

The next step is to provide the value that will be given to the child components that need it.

We do this by creating an object with properties and values that we want to pass to the consumer. We do this with the .Provider option

So for example, we created a context component called PostContext, so let’s use this to wrap the JSX that will need it. Here’s our App function, which currently does NOT use context…

function App() {
const [posts, setPosts] = useState(() =>
Array.from({ length: 30 }, () => createRandomPost())
);
const [searchQuery, setSearchQuery] = useState("");
const [isFakeDark, setIsFakeDark] = useState(false);

// Derived state. These are the posts that will actually be displayed
const searchedPosts =
searchQuery.length > 0
? posts.filter((post) =>
`${post.title} ${post.body}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
: posts;

function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}

function handleClearPosts() {
setPosts([]);
}

// Whenever `isFakeDark` changes, we toggle the `fake-dark-mode` class on the HTML element (see in "Elements" dev tool).
useEffect(
function () {
document.documentElement.classList.toggle("fake-dark-mode");
},
[isFakeDark]
);

return (
<section>
<button
onClick={() => setIsFakeDark((isFakeDark) => !isFakeDark)}
className="btn-fake-dark-mode"
>
{isFakeDark ? "☀️" : "🌙"}
</button>

<Header
posts={searchedPosts}
onClearPosts={handleClearPosts}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<Main posts={searchedPosts} onAddPost={handleAddPost} />
<Archive onAddPost={handleAddPost} />
<Footer />
</section>
);
}

You can see that we’re rendering a <Header> component, and passing in props to that header component. The props we’re passing in are as follows:

        posts={searchedPosts}
onClearPosts={handleClearPosts}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}

Let’s now instead use context with PostContext.Provider

  return (
<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
}}
>
<section>
<button
onClick={() => setIsFakeDark((isFakeDark) => !isFakeDark)}
className="btn-fake-dark-mode"
>
{isFakeDark ? "☀️" : "🌙"}
</button>

<Header
posts={searchedPosts}
onClearPosts={handleClearPosts}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<Main posts={searchedPosts} onAddPost={handleAddPost} />
<Archive onAddPost={handleAddPost} />
<Footer />
</section>
</PostContext.Provider>
);
}

So we’ve created a new object as our “value” that will be consumed, and the object contains a prop with a piece of derived state (searchedPosts), a function called handleAddPost, and another function called handleClearPosts.

// Derived state. These are the posts that will actually be displayed
const searchedPosts =
searchQuery.length > 0
? posts.filter((post) =>
`${post.title} ${post.body}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
: posts;

// function
function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}

// function
function handleClearPosts() {
setPosts([]);
}

We may also potentially need to consume the searchQuery piece of state and the function to set it (setSearchQuery) so let’s add those as well to our object just in case…

<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery: searchQuery,
setSearchQuery: setSearchQuery,
}}
>

Note: when the prop and value are the same name, we can shorten them to this…

<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery,
setSearchQuery,
}}
>

The next step is to actually use (consume) the context in the component, so we also need to bring in the useContext() hook from the React library…

import { useEffect, useState, createContext, useContext } from "react";

and now if we go to the component that needs the context, we’ll start to consume it. BEfore we do that, let’s look at one of the components that will need the context we’ve created….. the Header component….this is BEFORE we utilize context:

function Header({ posts, onClearPosts, searchQuery, setSearchQuery }) {
return (
<header>
<h1>
<span>⚛️</span>The Atomic Blog
</h1>
<div>
<Results posts={posts} />
<SearchPosts
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<button onClick={onClearPosts}>Clear posts</button>
</div>
</header>
);
}

So we can see in our header component, that currently it’s receiving props, and then using them.

Let’s break this down a bit more… so our Header component has a couple of child components (Results and SearchPosts) and we’re essentially “prop drilling” some of the props into our Header component, just so that we can pass them onto those child components, but the one prop that the Header component itself requires is the onClearPosts prop as this will triggler the handleClearPosts function when the button in our Header component is clicked.

So here, we can utilize one of the objects in our context to deal with this.

So let’s just for now get rid of onClearPosts ouut of the list of props….

// onClearposts prop removed...
function Header({ posts, searchQuery, setSearchQuery }) {
return (

and now consume this value in our button using the useContext() hook. We do this by first de-structuring the object we want (onClearPosts) from the context we created, and then we reference it…

function Header({ posts, searchQuery, setSearchQuery }) {

// de-structure the one we want
const { onClearPosts } = useContext(PostContext);

return (
<header>
<h1>
<span>⚛️</span>The Atomic Blog
</h1>
<div>
<Results posts={posts} />
<SearchPosts
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
// now consume it
<button onClick={onClearPosts}>Clear posts</button>
</div>
</header>
);
}

Now let’s take a look at the child components… firstly the Results component.

We can see that Results utilizes the “posts” prop… let’s remove this from the Header props…

// onClearposts prop and posts prop removed...
function Header({ searchQuery, setSearchQuery }) {
return (

Let’s also now remove this prop so that it’s not being passed to the Results component.

So this…

<Results posts={posts} />

becomes this…

<Results />

But of course the Results component still needs access to this derived state, so now we tell the Results component itself to consume what we need.

Here’s the Results component BEFORE we utilize context…

function Results({ posts }) {
return <p>🚀 {posts.length} atomic posts found</p>;
}

and here’s what it’s like after…

function Results() {
const { posts } = useContext(PostContext);
return <p>🚀 {posts.length} atomic posts found</p>;
}

So again, you can see that we do not need to pass in any props to the component, we simply retrieve the property we need from our context value object, and then use it.

Let’s do the same for our SearchPosts child component…. firstly, we’ll update our Header (parent) component to remove the props that are being drilled down…

function Header() {
const { onClearPosts } = useContext(PostContext);

return (
<header>
<h1>
<span>⚛️</span>The Atomic Blog
</h1>
<div>
<Results />
<SearchPosts />
<button onClick={onClearPosts}>Clear posts</button>
</div>
</header>
);
}

So as you can see, we’re not passing any props at all down to our child components now.

Now let’s update our SearchPosts child component…. what used to be this….

function SearchPosts({ searchQuery, setSearchQuery }) {
return (
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search posts..."
/>
);
}

Now becomes this….

function SearchPosts() {
const { searchQuery, setSearchQuery } = useContext(PostContext);

return (
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search posts..."
/>
);
}

Advanced Patterns with Custom hooks

We now have a full project that utilizes the context api and works correctly…

import { useEffect, useState, createContext, useContext } from "react";
import { faker } from "@faker-js/faker";

function createRandomPost() {
return {
title: `${faker.hacker.adjective()} ${faker.hacker.noun()}`,
body: faker.hacker.phrase(),
};
}

const PostContext = createContext();

function App() {
const [posts, setPosts] = useState(() =>
Array.from({ length: 30 }, () => createRandomPost())
);
const [searchQuery, setSearchQuery] = useState("");
const [isFakeDark, setIsFakeDark] = useState(false);

// Derived state. These are the posts that will actually be displayed
const searchedPosts =
searchQuery.length > 0
? posts.filter((post) =>
`${post.title} ${post.body}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
: posts;

function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}

function handleClearPosts() {
setPosts([]);
}

// Whenever `isFakeDark` changes, we toggle the `fake-dark-mode` class on the HTML element (see in "Elements" dev tool).
useEffect(
function () {
document.documentElement.classList.toggle("fake-dark-mode");
},
[isFakeDark]
);

return (
<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery,
setSearchQuery,
}}
>
<section>
<button
onClick={() => setIsFakeDark((isFakeDark) => !isFakeDark)}
className="btn-fake-dark-mode"
>
{isFakeDark ? "☀️" : "🌙"}
</button>

<Header />
<Main />
<Archive />
<Footer />
</section>
</PostContext.Provider>
);
}

function Header() {
const { onClearPosts } = useContext(PostContext);

return (
<header>
<h1>
<span>⚛️</span>The Atomic Blog
</h1>
<div>
<Results />
<SearchPosts />
<button onClick={onClearPosts}>Clear posts</button>
</div>
</header>
);
}

function SearchPosts() {
const { searchQuery, setSearchQuery } = useContext(PostContext);

return (
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search posts..."
/>
);
}

function Results() {
const { posts } = useContext(PostContext);
return <p>🚀 {posts.length} atomic posts found</p>;
}

function Main() {
return (
<main>
<FormAddPost />
<Posts />
</main>
);
}

function Posts() {
return (
<section>
<List />
</section>
);
}

function FormAddPost() {
const { onAddPost } = useContext(PostContext);
const [title, setTitle] = useState("");
const [body, setBody] = useState("");

const handleSubmit = function (e) {
e.preventDefault();
if (!body || !title) return;
onAddPost({ title, body });
setTitle("");
setBody("");
};

return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Post body"
/>
<button>Add post</button>
</form>
);
}

function List() {
const { posts } = useContext(PostContext);
return (
<ul>
{posts.map((post, i) => (
<li key={i}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
);
}

function Archive() {
// Here we don't need the setter function. We're only using state to store these posts because the callback function passed into useState (which generates the posts) is only called once, on the initial render. So we use this trick as an optimization technique, because if we just used a regular variable, these posts would be re-created on every render. We could also move the posts outside the components, but I wanted to show you this trick 😉

const { onAddPost } = useContext(PostContext);

const [posts] = useState(() =>
// 💥 WARNING: This might make your computer slow! Try a smaller `length` first
Array.from({ length: 10000 }, () => createRandomPost())
);

const [showArchive, setShowArchive] = useState(false);

return (
<aside>
<h2>Post archive</h2>
<button onClick={() => setShowArchive((s) => !s)}>
{showArchive ? "Hide archive posts" : "Show archive posts"}
</button>

{showArchive && (
<ul>
{posts.map((post, i) => (
<li key={i}>
<p>
<strong>{post.title}:</strong> {post.body}
</p>
<button onClick={() => onAddPost(post)}>Add as new post</button>
</li>
))}
</ul>
)}
</aside>
);
}

function Footer() {
return <footer>&copy; by The Atomic Blog ✌️</footer>;
}

export default App;

However it’s common for developers to want to tidy this code up a bit, by moving some of the code into a separate “Provider” file, along with a custom hook.

We’ll extract the following code and place it in a new file called PostContext.js

import { createContext, useState, useContext } from "react";
import { faker } from "@faker-js/faker";

const PostContext = createContext();

function createRandomPost() {
return {
title: `${faker.hacker.adjective()} ${faker.hacker.noun()}`,
body: faker.hacker.phrase(),
};
}

function PostProvider({ children }) {
const [posts, setPosts] = useState(() =>
Array.from({ length: 30 }, () => createRandomPost())
);
const [searchQuery, setSearchQuery] = useState("");

// Derived state. These are the posts that will actually be displayed
const searchedPosts =
searchQuery.length > 0
? posts.filter((post) =>
`${post.title} ${post.body}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
: posts;

function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}

function handleClearPosts() {
setPosts([]);
}

return (
<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery,
setSearchQuery,
}}
>
{children}
</PostContext.Provider>
);
}

function usePostContext() {
const context = useContext(PostContext);

if (context === undefined) {
throw new Error("usePostContext must be used within a PostProvider.");
}

return context;
}

export { PostProvider, usePostContext };

So we have a named “provider” called PostProvider, and we’ve also created a custom hook called usePostContext()

Let’s first look at the PostProvider part…

We’ve added all the state logic and handlers into the function, and are then returning the actual provider and {children} props as JSX

We then export this as a named provider.

So now in App.js we can import this custom provider, and then wrap our components that need the context in our custom provider….

import { PostProvider, usePostContext } from "./PostContext";
  return (
<PostProvider>
<section>
<button
onClick={() => setIsFakeDark((isFakeDark) => !isFakeDark)}
className="btn-fake-dark-mode"
>
{isFakeDark ? "☀️" : "🌙"}
</button>

<Header />
<Main />
<Archive />
<Footer />
</section>
</PostProvider>
);

everything between the opening and closing tag for PostProvider will then be the children we are passing to our provider, in just the same way as we did when we wrapped them inside <PostContext.Provider> before.

The next thing is the custom hook, which is the usePostContext function

function usePostContext() {
const context = useContext(PostContext);

if (context === undefined) {
throw new Error("usePostContext must be used within a PostProvider.");
}

return context;
}

we also import this, and whenever we need to use the context value, we then reference this custom hook.

So this…

const { onClearPosts } = useContext(PostContext);

becomes this…

const { onClearPosts } = usePostContext();

--

--

Jeff P
Jeff P

Written by Jeff P

I tend to write about anything I find interesting. There’s not much more to it than that really :-)

No responses yet