Skip to content

Provider Components

Video Summary

Let's suppose we have 3 different pieces of state we want to pass through context. We set it up with 3 different context providers, like so:

export const UserContext = React.createContext();
export const ThemeContext = React.createContext();
export const PlaybackRateContext = React.createContext();
function App() {
const [user, setUser] = React.useState(null);
const [theme, setTheme] = React.useState('light');
const [playbackRate, setPlaybackRate] = React.useState(1);
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<PlaybackRateContext.Provider value={playbackRate}>
<Homepage />
</PlaybackRateContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}

It often surprises developers that we'd want to use multiple contexts. Wouldn't it be simpler to group them all together, like this?

export const AppContext = React.createContext();
function App() {
const [user, setUser] = React.useState(null);
const [theme, setTheme] = React.useState('light');
const [playbackRate, setPlaybackRate] = React.useState(1);
return (
<AppContext.Provider value={{ user, theme, playbackRate }}>
<Homepage />
</AppContext.Provider>
);
}

While this might look simpler at first glance, there are two reasons why it's an established best practice to create individual contexts for each discrete concern:

  1. There are potential performance benefits (we'll learn more about this later in the course).
  2. It can improve code readability.

That second point might be surprising: after all, the code seems more readable with a single AppContext, doesn't it?

Well, here's the thing: this example is wildly unrealistic. In reality, each concern often has a bunch of other stuff.

Let's look at a more realistic example:

import React from 'react';
import useSWR from 'swr';
import { COLORS } from './constants';
import Homepage from './Homepage';
export const UserContext = React.createContext();
export const ThemeContext = React.createContext();
export const PlaybackRateContext = React.createContext();
const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-current-user';
async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();
if (!json.ok) {
throw json;
}
return json.user;
}
function App() {
const {
data: user,
error: userError,
mutate: mutateUser,
} = useSWR(ENDPOINT, fetcher);
const [theme, setTheme] = React.useState(() => {
return window.localStorage.getItem('color-theme') || 'light';
});
const [playbackRate, setPlaybackRate] = React.useState(1);
React.useEffect(() => {
window.localStorage.setItem('color-theme', theme);
}, [theme]);
const toggleTheme = React.useCallback(() => {
setTheme((currentTheme) => {
return currentTheme === 'light' ? 'dark' : 'light';
});
}, []);
const colors = COLORS[theme];
const logOut = React.useCallback(() => {
mutateUser({
user: null,
});
}, [mutateUser]);
const editProfile = React.useCallback((newData) => {
mutateUser({
user: {
...user,
...newData
},
});
}, [user, mutateUser]);
const resetPlaybackRate = React.useCallback(() => {
setPlaybackRate(1);
}, []);
return (
<UserContext.Provider value={{ user, logOut, editProfile }}>
<ThemeContext.Provider value={{
theme,
toggleTheme,
colors,
}}>
<PlaybackRateContext.Provider
value={{
playbackRate,
setPlaybackRate,
resetPlaybackRate,
}}
>
<Homepage />
</PlaybackRateContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
export default App;

This is a lot to take in, but essentially, each of our 3 concerns has additional stuff:

  • The user object is now being fetched with SWR, like we learned in the last module. We also have some helper functions for common actions, like logging out or editing the user
  • The theme state is being persisted in localStorage, like we saw in the Effect exercises. It also has a derived colors variable, and a toggleTheme helper function
  • The playbackRate state has a resetPlaybackRate helper function.

As a result, App.js feels really cluttered, and it's hard to understand what's going on here.

To improve this situation, we're going to use the provider component pattern.

First, we'll create a new component, UserProvider, in a new file. This component will manage everything related to the “user” concern.

Everything related to the user gets moved over:

import React from 'react';
import useSWR from 'swr';
export const UserContext = React.createContext();
const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-current-user';
async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();
if (!json.ok) {
throw json;
}
return json.user;
}
function UserProvider({ children }) {
const { data: user, error: userError, mutate: mutateUser } = useSWR(
ENDPOINT,
fetcher
);
const logOut = React.useCallback(() => {
mutateUser({
user: null,
});
}, [mutateUser]);
const editProfile = React.useCallback(
(newData) => {
mutateUser({
user: {
...user,
...newData,
},
});
},
[user, mutateUser]
);
return (
<UserContext.Provider
value={{ user, logOut, editProfile }}
>
{children}
</UserContext.Provider>
);
}
export default UserProvider;

We'll export the context itself as a named export, and the component as the default export.

This component is ultimately a wrapper around UserContext.Provider, a component we get from React (by calling React.createContext()).

Inside App, we'd use UserProvider in the same spot we'd have used UserContext.Provider:

// App.js
function App() {
// Theme and playbackRate stuff unchanged
return (
<UserProvider>
<ThemeContext.Provider value={{
theme,
toggleTheme,
colors,
}}>
<PlaybackRateContext.Provider
value={{
playbackRate,
setPlaybackRate,
resetPlaybackRate,
}}
>
<Homepage />
</PlaybackRateContext.Provider>
</ThemeContext.Provider>
</UserProvider>
);
}

We also need to update the components that consume the context, so that we're updating it from this new file:

// Homepage.js
import { UserContext } from './UserProvider';

There are two benefits to using this pattern:

  1. Everything related to the user concern—including the state, the data-fetching, and the context—is grouped in 1 spot.
  2. The App component is decluttered, letting us quickly understand how the application is structured, without drowning in the details.

Here's how our App component would look if we created provider components for all 3 concerns:

import Homepage from './Homepage';
import UserProvider from './UserProvider';
import ThemeProvider from './ThemeProvider';
import PlaybackRateProvider from './PlaybackRateProvider';
function App() {
return (
<UserProvider>
<ThemeProvider>
<PlaybackRateProvider>
<Homepage />
</PlaybackRateProvider>
</ThemeProvider>
</UserProvider>
);
}

This pattern can be difficult to parse, but hopefully it'll make more sense once you try it for yourself, below!

Practice - extracting two more provider components

The sandbox below picks up where the video left off. The UserProvider component has been created, and it's up to you to create two new provider components: ThemeProvider and PlaybackRateProvider.

Acceptance Criteria:

  • The ThemeProvider component should manage everything related to the theme state and context.
  • The PlaybackRateProvider component should manage everything related to the playbackRate state and context.
  • App.js should import and use these two new components, matching the style/format of the UserProvider component.
  • Inside Homepage.js, the imports should be updated, so that we're importing the contexts from the provider components, not from App

Code Playground

import React from 'react';

import { COLORS } from './constants';
import Homepage from './Homepage';
import UserProvider from './UserProvider';
import ThemeProvider from './ThemeProvider';
import PlaybackRateProvider from './PlaybackRateProvider';

export const ThemeContext = React.createContext();
export const PlaybackRateContext = React.createContext();

function App() {
const [theme, setTheme] = React.useState(() => {
return window.localStorage.getItem('color-theme') || 'light';
});

const [playbackRate, setPlaybackRate] = React.useState(1);

React.useEffect(() => {
window.localStorage.setItem('color-theme', theme);
}, [theme]);

const toggleTheme = React.useCallback(() => {
setTheme((currentTheme) => {
return currentTheme === 'light' ? 'dark' : 'light';
});
}, []);

const colors = COLORS[theme];

const resetPlaybackRate = React.useCallback(() => {
setPlaybackRate(1);
}, []);

return (
<UserProvider>
<ThemeContext.Provider
value={{
theme,
toggleTheme,
colors,
}}
>
<PlaybackRateContext.Provider
value={{
playbackRate,
setPlaybackRate,
resetPlaybackRate,
}}
>
<Homepage />
</PlaybackRateContext.Provider>
</ThemeContext.Provider>
</UserProvider>
);
}

export default App;