Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris
The React Context exists so you don’t have to pass in data manually at every level. Context is about sharing data to many components. So essentially it's about Publish and Subscribe or Pubsub for short.
In this article we will learn about the following topics:
- The context API, what it is and why use it
- Building blocks, we will learn about the different building blocks using a Context and how they work together
- In action, we will look at several examples of creating a Context and we will ensure we learn about Dynamic contexts and how HOC, higher order components can help clean up our Context code
If you ever get lost following the code samples in the article, have a look at this demo containing all the code shown below:
https://github.com/softchris/react-context-demo
Why Context API and what is it?
The React Context exists so you don’t have to pass in data manually at every level. Context is about sharing data to many components. The reason we need the Context API is that it’s cumbersome to pass data from parent to child via props if there are many components requiring the same data. By using the Context API we no longer pass this kind of shared data with props.
When to use /not use it
Things that belong in a context is data that is considered global like a user or a cart etc. So let’s try to list the different reasons for using a Context:
- data needed in many places , data that needs to be used by many components like a theme, user or a cart
-
pass props through many components , sometimes it’s better to use composition over context when you want to pass a
props
value through many components
Building blocks and API
The Context API consists of some building blocks that it is important that we know about what they are called but also what their role is:
- context , the context object is an object holding the current context value and can be subscribed to
- provider , This is a React component that provides the value in question, it grabs it from the context object
-
consumer , This is a React component that is able to consume the value provided by the
Provider
and is able to show the value
This is all a bit theoretical and may sound a little confusing so let’s dive right into an example to clear any confusion.
Context API in action
To utilize a Context in React we need to do the following:
-
create a context, we do this with a call to React.createContext(), this will return a Context object that exposes a
Provider Component
as well as aConsumer Component
-
declare a provider, this is us grabbing the
Provider
Component reference available in the context object we just created - declare a consumer, this is also a component that lives on the Context object and we use this to show the value to the user
Creating a Context object
Let’s start by creating a React project using Create React App, CRA:
npx create-react-app context-demo
cd context-demo
npm start
Good, we have a project. Now let’s create a file called theme.js
, it will hold our Context
object.
It’s quite straight-forward to create a Context object. For that we use the React.createContext()
method like so:
// theme.js
import React from 'react';
const ThemeContext = React.createContext('light');
export default ThemeContext;
Above we call createContext()
and we give it an input parameter which is simply the default value we want the context to have. We also export the object itself so we can use it in other places.
That’s very little code to write to use something as powerful as a Context
. We haven’t seen anything yet though, so the fun has just begun:
Declare a Provider
Ok, so we have a Context
object let’s grab a reference to a Provider
next. For this, we will first create a component file Sample.js
, you can really call it anything you want but the point is to have a React component to demonstrate how the Context
object works. Let’s create a component:
// Sample.js
import React from 'react';
import Theme from './theme';
const Sample = () => (
<Theme.Provider value='dark'>
// declare consumer
</Theme.Provider>
);
export default Sample;
Above we are declaring a normal functional React component and we also import our Theme
, our Context
object. We then grab a reference to our provider by calling Theme.Provider
. At this point, nothing really works, because we are lacking a Consumer
component that can actually consume the value and thereby show it to a user.
Furthermore, we also set the value property to dark
.
Wait wait, hold on… Didn’t we just set the value to light in our theme.js
file, what’s the point of doing that if we are going to override it in the Provider
anyway? Very good question, let’s save it a bit until we declared a Consumer and then it will all make sense.
Declare a Consumer
So next up is about declaring a Consumer
component and show how we can show the value to the user. Let’s add that to our code:
// Sample.js
import React from 'react';
import Theme from './theme';
const Sample = () => (
<Theme.Provider value='dark'>
<Theme.Consumer>
{theme => <div>Our theme is: {theme}</div>}
</Theme.Consumer>
</Theme.Provider>
);
export default Sample;
Above we added our Consumer
, in the form of Theme.Consumer
component and we can see that we inside it define a function whose parameter is our theme value. We are then able to show the theme
value in a div.
Ok then, let’s get back to our question, why are we setting the value property in our Theme.Provider
component if we already set a default value in our theme.js
file, here:
// theme.js
import React from 'react';
const ThemeContext = React.createContext('light');
export default ThemeContext;
Well, the default value above won’t be used if we declare a Provider
. If we are missing a Provider
component, however, it will use the default value as a fallback. So the following code will output dark
as value, which is the value we give to the Provider
:
const Sample = () => (
<Theme.Provider value='dark'>
<Theme.Consumer>
{theme => <div>Theme value: {theme}</div>}
</Theme.Consumer>
</Theme.Provider>
)
whereas this code will output light
as value, e.g the default value:
// Sample.js with a Provider missing
const Sample = () => (
<Theme.Consumer>
{theme => <div>Theme value: {theme}</div>}
</Theme.Consumer>
);
Usage
Taking our Context
for a spin means we need to create a Provider
and a Consumer
as we did in the last section, however, most likely the Consumer
part is baked into a Component like so:
// ThemedButton.js
import Theme from 'theme.js';
const ThemedButton = (props) => (
<Theme.Consumer>
{theme => <button { ...props }>button with them: {theme}</button>}
</Theme.Consumer>
);
export default ThemedButton
This means that our code from the last section can be cleaned up somewhat to look like this:
// Sample.js
import React from 'react';
import Theme from './theme';
import ThemedButton from './ThemedButton';
const Sample = () => (
<Theme.Provider value='dark'>
<ThemedButton />
</Theme.Provider>
);
export default Sample;
As you can see the value from the Provider
is being passed down through the props
and we can inside of the ThemedButton
component access the theme
property through the Consumer
.
Dynamic Context
What if we want to change the provider value? One way of doing that is by having a dynamic context. We can achieve that by placing our Provider
inside of a component and let its value depend on the component state like so:
// AnyComponent.js
import React from 'react';
class AnyComponent extends React.Component {
state = {
theme: 'dark'
};
render() {
return (
<ThemeContext.Provider value={ this.state.theme }>
<ThemedButton />
</ThemeContext.Provider>
);
}
}
Now it’s easy for us to change the state and thereby we can change the value the Provider
is providing to any Consumer
.
Changing the state example
Below we are creating a component containing a droplist with two different values light
and dark
and when we switch between the values, the state is altered and because the state is connected to the Provider
, the provided value is changed with it.
Let’ look at the code:
// AnyComponent.js
import React from 'react';
import Theme from './theme';
import ThemedButton from './ThemedButton';
class AnyComponent extends React.Component {
state = {
theme: 'dark',
themes: ['light', 'dark']
};
handleSelect = (evt) => {
console.log('Changing value to ' + evt.target.value);
this.setState({
theme: evt.target.value
});
};
render() {
return (
<React.Fragment>
<h2>Any component</h2>
<select value = {this.state.theme}
onChange ={this.handleSelect}>
{ this.state.themes.map(t =>
<option value = {t} >{t}</option>)
}
</select>
<div>
Selected theme: {this.state.theme}
</div>
<Theme.Provider value ={this.state.theme}>
<ThemedButton theme={this.state.theme} />
</Theme.Provider>
</React.Fragment>
);
}
}
export default AnyComponent;
We can see from the above code that when the onChange
event is triggered we invoke the handleSelect()
method and that leads to the state property theme
being updated. That same property theme is what the Theme.Provider
is assigning as its value
attribute. Thereby a change in the droplist leads to the Provider
component providing a new value. A fairly simple code flow but it does show where we should change things to get the Consumer component to display a new value.
Second example — a cart
Our next example is a little bit different. We have seen how we can expose a value from a components state and make that the value of the Provider
component and thereby we can affect what the Provider provides. This can be taken to a further level. Further in the sense that we can not only expose the value of a Provider to a Consumer but also methods, methods that will allow us to change the provided value. Let’s look at some code for this:
// cart.js
import React from 'react';
const CartContext = React.createContext({
cart: void 0,
addItem: () => {}
});
export default CartContext;
We start with creating our Context
object and this time we give it a more complex data type than a string or a number. The input parameter to createContext()
method is an object {}
with a property cart
.
A note void 0
is just the same as undefined
.
Next, we will create two different components:
-
CartPage, this will contain our Consumer component and thereby display the value from our
Context
object -
CartProvider, this will be a component that will not only provide the value from the
Context
object but also expose a method with which we can change the provided value
Let’s start with CartPage
component:
// CartPage.js
import React from 'react';
import CartContext from './cart';
const products = [{
id: 1,
title: 'Fortnite'
}, {
id: 2,
title: 'Doom'
}, {
id: 3,
title: 'Quake'
}]
const CartPage = () => (
<CartContext.Consumer>
{({ cart, addItem }) => (
<React.Fragment>
<div>
<h2>Product list</h2>
{products.map(p => <button onClick={() => addItem(p)} value={p}>{p.title} . </button>)}
</div>
<div>
<h2>Cart</h2>
{cart.map(item => <div> {item.title} </div>)}
</div>
</React.Fragment>
)}
</CartContext.Consumer>
);
export default CartPage;
We see above that we use CartContext
component and that we define and display our cart value, but there is an addition to it in the form of the addItem()
method. This method will allow us to change the cart item, but how you ask? Let’s have a look at our CartProvider
component next to find out the answer:
import React from 'react';
import CartPage from './CartPage';
import CartContext from './cart';
class CartProvider extends React.Component {
constructor() {
super();
this.state = {
cart: [],
addItem: (item) => {
this.setState({
cart: [...this.state.cart, { ...item }]
})
}
}
}
render() {
return (
<CartContext.Provider value = {this.state} >
<CartPage />
</CartContext.Provider>
);
}
}
export default CartProvider;
We can see here that the state object consists of the properties cart
and addItem
and what gets passed into the value property of the CartContext.Provider
is this.state e.g both cart
and addItem()
. This means we could easily expand this with a removeItem()
function or whatever we need, this is how we get more than just a value exposed to a Consumer
component.
Higher order component
Sometimes a context needs to be provided in many places. In our example above imagine the cart
being used inside of a header that wants to show how many items you have in a cart. There might also be dedicated Cart Page where you can see the cart content more in detail. It might get tedious to have to wrap all those component content in a Consumer
tag. For those situations, it’s better to use a HOC, a higher order component. This means we can create a function where we use our component as input and we augment the context data.
It can look like the following:
// withCart.js
import CartContext from './cart';
import React from 'react';
export const withCart = (Component) => {
return function fn(props) {
return (
<CartContext.Consumer>
{(context) => <Component {...props} {...context} />}
</CartContext.Consumer>
);
};
};
As you can see above, we are using a Consumer
to make this happen but we also use the spread parameter { ...context}
to transfer what is in the context object to the underlying component. Now we can easily use this function to decorate our component, like so:
// Header.js
import React from 'react';
import withCart from './withCart';
class Header extends React.Component {
render() {
const { cart } = this.props;
return (
{cart.length === ?
<div>Empty cart</div> :
<div>Items in cart: ({cart.length})</div>
}
);
}
}
const HeaderWithCart = withCart(Header);
export default HeaderWithCart;
Summary
In this article, we have covered quite a lot. We have explained what the Context
API is and when to use it. We also talked about its building blocks Provider
and Consumer
. Furthermore, we have covered how to update provided values and lastly how we can clean up a bit using a HOC, a higher order component. Hopefully, you have found this useful. :)
Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris