Provider Pattern
Make data available to multiple child components
Overview
The Provider Pattern uses React's Context
API - which is a way to easily share data between components.
Let's say that we want to add a theme toggle to our landing page, where users can switch between light mode and dark mode.
Several components change their style based on the currently active theme, such as the TopNav
, the Listing
cards, the Main
section, and the Toggle
.
With the Provider pattern, we can share the theme state across multiple components throughout our application. The provider provides this context to components, which in turn consume this data.
Prop-drilling
Before the Context API was available, we often ended up we often end up with something called prop drilling when we wanted to share data across multiple components. This is the case when we pass props far down the component tree.
However, passing props down this way can get quite insecure and complex, and it's simply not a very scalable approach. We cannot easily rename a prop
, or restructure the component tree. It can also easily lead to a decreased performance, since all child components need to re-render on a state update, even if they aren't consuming that data.
The provider pattern solves this by exposing the values of the Context
to all children within a Provder
. A component can optionally consume this data, making it possible to pass data to multiple components without prop drilling.
Only the components that care about the data get re-rendered when the state updates.
Implementation
A Provider
is a higher order component provided to us by the Context
object. We can create a Context
object, using the createContext
method that React provides for us.
export const ThemeContext = React.createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Any component wrapped in the ThemeProvider
now has access to the theme
and setTheme
properties.
import { ThemeProvider, ThemeContext } from "../context";
const LandingPage = () => {
<ThemeProvider>
<TopNav />
<Main />
</ThemeProvider>;
};
const TopNav = () => {
return (
<ThemeContext.Consumer>
{{ theme }} =>{" "}
<div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}>
...
</div>{" "}
}
</ThemeContext.Consumer>
);
};
const Toggle = () => {
return (
<ThemeContext.Consumer>
{{ theme, setTheme }} => (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
style={{
backgroundColor: theme === "light" ? "#fff" : "#000",
color: theme === "light" ? "#000" : "#fff",
}}
>
Use {theme === "light" ? "Dark" : "Light"} Theme
</button>
)}
</ThemeContext.Consumer>
);
};
However, we can also combine the Provider with the Hooks pattern. Instead of wrapping components in a <ThemeContext.Consumer>
component, we can use the built-in useContext
hook.
export const ThemeContext = React.createContext(null);
export function useThemeContext() {
const { theme, setTheme } = useContext(ThemeContext);
return { theme, setTheme };
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Each component that needs to have access to the ThemeContext
, can now simply use the useThemeContext
hook.
import { useThemeContext } from "../context";
const LandingPage = () => {
<ThemeProvider>
<TopNav />
<Main />
</ThemeProvider>;
};
const TopNav = () => {
const { theme } = useThemeContext();
return (
<div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}>
...
</div>
);
};
const Toggle = () => {
const { theme, setTheme } = useThemeContext();
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
style={{
backgroundColor: theme === "light" ? "#fff" : "#000",
color: theme === "light" ? "#000" : "#fff",
}}
>
Use {theme === "light" ? "Dark" : "Light"} Theme
</button>
);
};
By creating hooks for the different contexts, it's easy to separate the providers's logic from the components that render the data.
Tradeoffs
Scalability: There's less risk involved when sharing state across multiple components with the Provider Pattern, as we can easily rename values when our application grows, and easily reuse components.
Performance: Components that consume the Provider
's context re-render
whenever a value changes. This can cause performance issues If you aren't
careful which components are consuming the context.
Exercise
Challenge
The application below contains a Listings
component and an Input
component that both use the useListings
hook. However, this results in two separate calls to the API. Refactor this code so that it uses a ListingsProvider
that provides the listings data to both components.