Curious about React Context, using an HoC to generalize a context consumer, why you might need to use contextType, or what is prop-drilling? π€
If yes, cool! Read on because this might be the guide that'll help you get started with context.
Intro: Why you need React Context ?
Let's say you have a Card
component that gets the style from the current theme of App
, so you end up passing the theme from App
to Card
, involving all the components in between unnecessarily.
App --theme-->
Β Container --theme-->
Β Β Section --theme-->
Β Β Β ThemedCard --theme-->
Β Β Β Β Card
In code, it might look like this:
// Card.jsx
import React from 'react';
import styles from './styles';
const Card = (props) => (
<div style={styles[props.theme]}>
<h1>Card</h1>
</div>
)
export default Card;
// App.jsx
import React from 'react';
const ThemedCard = (props) => <Card theme={props.theme} />
const Section = (props) => <ThemedCard theme={props.theme} />
const Container = (props) => <Section theme={props.theme} />
class App extends React.Component {
state = {
theme: 'dark',
}
switchTheme = () => {
const newTheme = this.state.theme === "dark" ? "default" : "dark";
this.setState({
theme: newTheme
});
};
render() {
return (
<div>
<button onClick={this.switchTheme}>Switch theme</button>
<Container theme={this.state.theme} />
</div>
);
}
}
export default App;
Code for part 1 here: https://codesandbox.io/s/94p2p2nwop
This is called prop-drilling, and this gets even worse if you have more layers of components between the data source and user. One really good alternative is using Context.
createContext
First thing is to create a context using React.createContext
.
// ThemeContext.jsx
import React from "react";
const ThemeContext = React.createContext();
export default ThemeContext;
Context Provider: <ThemeContext.Provider>
Now we can wrap all the context users with the Context Provider, and pass the value
that we want to 'broadcast'.
The value that we pass becomes the actual context later, so you can decide to put a single value or an entire object here.
Note: We choose to do
value={this.state}
so we later accesscontext.theme
. If we dovalue={this.state.theme}
, we access it viacontext
// App.jsx
...
import ThemeContext from "./ThemeContext";
...
return (
<div>
<button onClick={this.switchTheme}>Switch theme</button>
<ThemeContext.Provider value={this.state}>
<Container />
</ThemeContext.Provider>
</div>
);
...
So how do we access the theme
from its descendant Card
?
Context Consumer: <ThemeContext.Consumer>
To access the context, we use a context consumer <ThemeContext.Consumer>
from any ancestor of Card
.
Here we choose ThemedCard
so we keep the Card
presentational, without any context stuff.
Consumer gives access to the context and propagates it downwards.
The caveat is that it requires a function child that takes the context value as a prop and returns React node that uses the context value.
This is also known as a render prop pattern. More about render prop here.
<SomeContext.Consumer>
{(context_value) => (<div> ...do something with context_value </div>) }
</SomeContext.Consumer>
In our case, we render <Card>
taking the theme
from the context object.
We destructure theme using ({theme})
, but you can also do (context) => ...context.theme
, and/or add stuff to our App state and access them here via ({theme, name})
, which we will do later.
Note that we don't have to pass the theme
prop to Container anymore, and we also don't need the theme
prop from Section anymore, since we can 'tap' directly into the context using the Consumer.
// App.jsx
...
const ThemedCard = () => (
<ThemeContext.Consumer>
{({theme}) => <Card theme={theme} />}
</ThemeContext.Consumer>
)
...
const Section = () => <ThemedCard />
const Container = () => <Section />
Finally, we can use the theme in our Card to style it.
// Card.jsx
...
const Card = props => (
<div style={styles[props.theme]}>
<h1>Card</h1>
</div>
)
...
Code in part 2 here: https://codesandbox.io/s/5wrzoqp7ok
Now our context provider and consumer works great!
We have our root component <App />
that holds the state, propagating it through the Provider and a presentation component <ThemedCard />
that uses a Consumer to access the context and use it to style <Card />
.
Using a Higher Order Component (HoC) to generalize a Context container
Having a ThemedCard
is nice for theming Card
s but what if we want to theme other things, like an Avatar, Button, or Text. Does that mean we have to create Themed...
for each of these?
We could, but there is a better way to generalize the theming container so we can use it for any component we want to use our theme context.
withTheme HoC
A HoC in React is a function that takes a component and returns another component.
Instead of a ThemedWhatever
, we create a withTheme
HoC that returns a generic component ThemedComponent
that wraps ANY component we want to theme with the Context Consumer.
So whatever that component is: Card, Avatar, Button, Text, whatever, it would have access to our context! π
// withTheme.js
import React from "react";
import ThemeContext from "./ThemeContext";
const withTheme = Component => {
class ThemedComponent extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{({theme}) => <Component theme={theme} />}
</ThemeContext.Consumer>
);
}
}
return ThemedComponent;
};
export default withTheme;
Notice that the Consumer part is similar to the ones before, and the only thing that we added is the ThemedComponent
that wraps it.
But how do we use this HoC for Card?
using the HoC
We could toss the ThemedCard
! since we don't need it anymore! :yes:
Section can now render Card directly
// App.jsx
...
// remove/comment out const ThemedCard = () => ()
const Section = () => <Card />;
const Container = () => <Section />;
...
To use the HoC, we only need to call the HoC function withTheme
.
No other changes to our component, and it stays as presentational. We're just 'wrapping' it with out theme context.
export default withTheme(Card)
Here is the new version of Card
:
// Card.jsx
import React from 'react';
import withTheme from "./withTheme";
import styles from './styles';
const Card = (props) => (
<div style={styles[props.theme]}>
<h1>Card</h1>
</div>
)
export default withTheme(Card);
Code in part 3 here: https://codesandbox.io/s/9l82k7y2w
Nice! Now we have a HoC to theme components. We could also easily have a
Avatar
or Button
component that has access to the context.
For example:
const Avatar = props => (
<div style={styles[props.theme]}>
... all avatar stuff
)
export default withTheme(Avatar);
Access this.context
using contextType
Here's a little note about how flexible the HoC component can be.
What if, for some reason, you want to have lifecycle methods inside ThemedComponent
?
// withTheme.js
...
class ThemedComponent extends React.Component {
componentDidMount() {
// NO ACCESS TO context here π±
console.log(`current theme: ${ this.context.theme }`);
// -> ERROR: this.context is undefined β
}
render() {...}
...
React 16.6 introduced contextType
which allows you to access this.context
to:
- Access context inside the lifecycle methods
- Use context without using the render prop pattern
How? Just declare a static var in the class and assign it to the context object.
// withTheme.js
...
class ThemedComponent extends React.Component {
static contextType = ThemeContext;
componentDidMount() {
console.log(`current theme: ${ this.context.theme }`);
// -> current theme: dark β
}
...
We could also change our Consumer now to a simpler, more familiar syntax.
Instead of <ThemeContext.Consumer>{theme => <Component theme={theme}>}</ThemedContext.Consumer>
, we could do this:
// withTheme.js
...
render() {
return (
<Component theme={this.context.theme} />
);
}
Code in part 4: https://codesandbox.io/s/9l82k7y2w
That's more like it. Simple and less confusing brackets.
The only caveat with this is you're limited to subscribing to a single context with this. More on Multiple context here
Adding stuff to the context
As mentioned before, you can structure the data you expose in the context through the Provider any way you want, as long as you access it accordingly in the Consumer.
Let's say you add themes
in the context in the Provider...
Provider
// App.jsx
class App extends React.Component {
state = {
theme: 'dark',
themes: ['light', 'dark'],
}
...
In the Consumer, you can pass the entire this.context
instead
and you can pass the context as themeData
prop to <Card />
, and access its attributes from Card.
Consumer
// withTheme.js
...
render() {
return (
<Component themeData={this.context} />
);
}
...
// Card.jsx
...
const Card = ({themeData}) => (
<div style={styles[themeData.theme]}>
<h1>Cards</h1>
<p>{themeData.themes.toString()}</p>
</div>
)
...
Code in part 5 here: https://codesandbox.io/s/l2z1wxm8lq
That's all! I hope that helped clarify why you need context and the different ways of implementing it. Feel free to post any questions, comments or any suggestions.
If you want to learn React by building a mini-Spotify, and you like following slides, check out my React workshop repo
Happy context-ing π€!
Top comments (1)
This is so helpful, thanks!