React 16.3 finally made the Context API stable. Since then, many developers started using it to solve the “prop-drilling” problem — the issue where you need to pass a prop
down the component tree along components not making use of it in order to access that prop
in a component being rendered deep down the tree. The React documentation states the intent of the Context API as follows:
In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like this between components without having to explicitly pass a prop through every level of the tree.
Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.
Let’s look at the example of tracking the activity of the user. For that, we want to call a certain track
function every time the user clicks on a UI element.
const track = event => console.log(`${event} occured`);class App extends React.Component { render() { return <Toolbar track={track} />; }}function Toolbar(props) { return ( <div> <TrackedButton track={props.track} /> </div> );}function TrackedButton(props) { const onClick = () => { props.track("button click"); // do something on click }; return <Button onClick={onClick} />;}
We need the track
function in many different components of the app, and using React without any advanced patterns we need to pass track
through the whole component tree, even though App
and Toolbar
make no use of it.
We can use the new React Context API to inject the track
function into the needed components:
const track = event => console.log(`${event} occured`);const TrackerContext = React.createContext(track);class App extends React.Component { render() { return ( <TrackerContext.Provider value={track}> <Toolbar /> </TrackerContext.Provider> ); }}function Toolbar(props) { return ( <div> {/* does not need to forward props anymore */} <TrackedButton /> </div> );}function TrackedButton(props) { return ( <TrackerContext.Consumer> {value => ( <Button onClick={() => { // call track value("button click"); // do something on click }} /> )} </TrackerContext.Consumer> );}
We create a TrackerContext
, wrap the root component with Provider
which makes the track
function consumable as value
in the TrackedButton
component.
However, this is not something we couldn’t do prior to the Context
API, you could always do this with a HOC (Higher Order Component) in React. The code is even more readable using a HOC.
We can create a HOC, a function taking a component and returning a new component rendering the original component with some enhancements. In our case, we will just inject the track
function as a prop
into the inner component.
const track = event => console.log(`${event} occured`);const withTracker = track => Component => (props) => ( <Component track={track} {...props}/>);class App extends React.Component { render() { return ( /* no need to wrap root Component */ <Toolbar /> ); }}function Toolbar(props) { return ( <div> {/* need to use the HOC here */} <TrackedButtonWrapped /> </div> );}// TrackedButton is the same as in first solutionfunction TrackedButton(props) { const onClick = () => { props.track("button click"); // do something on click }; return <Button onClick={onClick} />;}// Need to create a higher-order component out of TrackedButtonconst TrackedButtonWrapped = withTracker(track)(TrackedButton)
The nice thing about this is that the code is a lot cleaner than with using Context
:
App
needs no ContextProvider
wrapper anymore.TrackedButton
is exactly the same component as in the plain React solution. It's not its responsibility anymore to retrieve the track
function. This is because we shifted the prop
retrieval into the **TrackedButtonWrapped**
HOC which we render in Toolbar
instead.So is React.createContext useless?
Let’s go back to the example given in the React documentation. There, Context
is used to theme an app.
The code is similar to the Tracking with Context example. ThemeContext.Provider
provides the theme
and a function to toggle the theme which sets state
in the root component. These variables are only consumed where needed - in the ThemedButton
to style the button and toggle the theme on button click.
const ThemeContext = React.createContext();class App extends React.Component { state = { theme: 'light' } toggleTheme = () => { this.setState(({ theme }) => ({ theme: theme === 'light' ? 'dark' : 'light', })); } render() { const value = { theme: this.state.theme, toggleTheme: this.toggleTheme, } return ( <ThemeContext.Provider value={value}> <Toolbar /> </ThemeContext.Provider> ); }}function Toolbar(props) { return ( <div> <ThemedButton /> </div> );}function ThemedButton(props) { return ( <ThemeContext.Consumer> {({ theme, toggleTheme }) => <Button theme={theme} onClick={toggleTheme}/>} </ThemeContext.Consumer> );}
Let’s try to apply the same HOC pattern that we did with Tracking
to Theming
.
Spoiler: It won’t work.
const withTheme = InnerComponent => class extends React.Component { state = { theme: 'light' } toggleTheme = () => { this.setState(({ theme }) => ({ theme: theme === 'light' ? 'dark' : 'light', })); } render() { return ( <InnerComponent theme={this.state.theme} toggleTheme={this.toggleTheme} /> ) }}class App extends React.Component { render() { return ( // again no Provider needed <Toolbar /> ); }}function Toolbar(props) { return ( <div> {/* use the HOC here */} <ThemedButtonWrapped /> {/* let's add another Button here */} <ThemedButtonWrapped /> </div> );}function ThemedButton(props) { return <Button onClick={props.toggleTheme} theme={props.theme} />;}// Need to create a higher-order component out of ThemedButtonconst ThemedButtonWrapped = withTheme(ThemedButton)
What’s wrong here? If we add another ThemedButton
, you'll notice that it doesn't work here. Every time the HOC is created, the component instance starts with a fresh state
, and so the buttons' themes are independent of each other. You can play around with it
Why did it work in the tracking example? In the tracking example the data (track
function) is static and never changed, so each Button instance using the same function is not a problem here.
This is the big advantage of the Context API
over the HOC pattern
and why it is so powerful: Using **Context**
the data is shared among all **Consumers**
.
Here’s again what the React documentation says about the use-cases of Context
:
Context is designed to share data that can be considered “global” for a tree of React components
I would go one step further and say that the Context API
is for global dynamic data that is used by multiple component instances. In the case of static data, you might not need Context
. It can always be replaced with a simpler to use HOC that injects the props
.
Here’s a general guideline how to decide whether to use React’s Context
API:
Originally published at cmichel.io