React: Context System
Intro
📓 Official Context Docs Reactjs: Context
Starting in version 16 of React there is a new context system to pass data through the component tree without having to pass it as a prop manually through every level of the tree. Some people feel that this new system is a replacement for a library called Redux, although that is not necessarily true.
In this post we are going to go over what the new context system does for us, how to use it, redacted syntax, and also how this all relates to Redux.
What It Does
Previously in React the only built in method to pass data from a parent to child component was to use the props system. Note that we are not including Redux in this, as Redux is a separate library. Props works great, however it can be tedious if we have a large DOM tree to be manually passing down a prop at every level of the tree to a deeply nested component. This is why the context system was introduced.
The context system allows us to pass information from a parent component to any child component, no matter how many layers below it may be. We can simply bypass having to pass that data at every layer. That's it, that is the whole purpose.
Scaffolding a Sample App
Let us create a quick sample application where we change the language of some text based on the selected language.
import React, { Component } from "react";
class App extends Component {
state = { language: "english" };
// change state of language when flag is clicked
onLanguageChange = (language) => {
this.setState({ language });
};
render() {
return (
<div className="ui container">
<div>
Select a language:
<i
className="flag us"
onClick={() => this.onLanguageChange("english")}
/>
<i
className="flag nl"
onClick={() => this.onLanguageChange("dutch")}
/>
</div>
{/* display language */}
<h3>{this.state.language}</h3>
</div>
);
}
}
export default App;
There is no translation yet, but we can see that we are toggling the state onClick
.
And now we need to create some additional child components so we can have something to pass the data down to. Let us craft three additional components quickly.
import React from 'react';
import Field from './Field';
import Button from './Button';
const UserCreate = () => {
return (
<div className="ui form">
<Field />
<Button />
</div>
);
}
export default UserCreate;
import React, { Component } from "react";
class Field extends Component {
render() {
return (
<div className="ui field">
<label> Name </label>
<input />
</div>
);
}
}
export default Field;
import React, { Component } from "react";
class Button extends Component {
render() {
return <button className="ui button primary"> Submit</button>;
}
}
export default Button;
And we can see that the field and button are nested inside of the UserCreate
component. All that gives us this.
Implementing Context
The context object is very similar to the Redux store, in the sense that it is an object which is keeping track of a set of data objects. Whether they be text, or pieces of state, the data that our components will need to behave correctly. In order to understand how to use the context object, we need to know how to get data into the object, and how to get data out.
Get Data In
To get data into the context object, we can create a series of context files. Each file will represent a specific set of context data, which we can then import into individual components to have access to that context. We will want to create a separate directory for our context files as well. Here is a bare context file.
import React from 'react';
export default React.createContext();
Default Value
First let us create a default value for this context. In this case we want our default language state to be english so we can pass that in as the default value.
import React from 'react';
export default React.createContext('english'); // highlight-line
Get Data Out
Then to reference this context we add the following to the child component (in this case Button
)
import React, { Component } from "react";
import LanguageContext from "../contexts/LanguageContext"; // highlight-line
class Button extends Component {
static contextType = LanguageContext; // highlight-line
render() {
return <button className="ui button primary"> Submit</button>;
}
}
export default Button;
The static
syntax is adding a property to our class Button
. An alternate syntax to accomplish this is the following.
import React, { Component } from "react";
import LanguageContext from "../contexts/LanguageContext"; // highlight-line
class Button extends Component {
render() {
return <button className="ui button primary"> Submit</button>;
}
}
Button.contextType = LanguageContext; // highlight-line
export default Button;
And our child component can now reference this.context
to get access to the default language state. If you console logged this.context
you would get "english".
So knowing that we can set up a little ternary operator so that the text on the button comes from a variable instead of a hardcoded string.
import React, { Component } from "react";
import LanguageContext from "../contexts/LanguageContext";
class Button extends Component {
static contextType = LanguageContext;
render() {
// if language is english use "submit", else use "voorleggen"
const text = this.context === 'english' ? 'Submit' : 'Voorleggen'; // highlight-line
return <button className="ui button primary"> {text}</button>;
}
}
export default Button;
And we can also do the same thing in our field component to change the label of the field. Now the only question is, how do we change the context when we click on the flag?
Update Context (Provider)
In order to update the context that we are now using in the component we need to use the provider function (similar but not the same as the redux provider) and wrap the parent component that required that context with the provider.
import React, { Component } from "react";
import UserCreate from "./UserCreate";
import LanguageContext from "../contexts/LanguageContext"; // highlight-line
class App extends Component {
state = { language: "english" };
// change state of language when flag is clicked
onLanguageChange = (language) => {
this.setState({ language });
};
render() {
return (
<div className="ui container">
<div>
Select a language:
<i
className="flag us"
onClick={() => this.onLanguageChange("english")}
/>
<i
className="flag nl"
onClick={() => this.onLanguageChange("dutch")}
/>
</div>
<LanguageContext.Provider value={this.state.language} > // highlight-line
<UserCreate />
</LanguageContext.Provider> // highlight-line
</div>
);
}
}
export default App;
We can then see that we provide a value to that provider, which is the data that we want to make available.
And we can see that the components are now updating correctly.
Accessing Data With Consumers
The second way to get information out of the context object is with the Consumer
component. The consumer component is created for us automatically when we create the context object.
import React, { Component } from "react";
import LanguageContext from "../contexts/LanguageContext";
class Button extends Component {
render() {
return (
<button className="ui button primary">
<LanguageContext.Consumer>
{(value) => value === 'english' ? 'Submit' : 'Voorleggen'}
</LanguageContext.Consumer>
</button>
);
}
}
export default Button;
The consumer has a strange syntax. It gets called with one child, which is always a function, and the argument of that function is the value of the context. With this method we no longer have to define the contextType
property in the component.
Accessing Multiple Contexts
The Consumer
component is required when we want to access multiple contexts inside one component. Whereas when we reference this.context
that can only be referring to one context type.
Multiple Contexts Sample
Let's look at an example where we are using two contexts within one component. In this case the button
component. We are going to make it blue when users click the dutch flag and red when they click the US flag.
First let us create a new context and then import it into the app. Then we make a new provider and we wrap our other provider in the new one. The order of the wrapping is irrelevant, the UserCreate
component just needs to be wrapped by both.
import React, { Component } from "react";
import UserCreate from "./UserCreate";
import LanguageContext from "../contexts/LanguageContext";
import ColorContext from "../contexts/ColorContext"; // highlight-line
class App extends Component {
state = { language: "english", color: "red" };
// change state of language when flag is clicked
changeLanguage = (language) => {
this.setState({ language });
};
// change color of button when flag is clicked
changeColor = (color) => {
this.setState({ color });
};
// bundle of functions for flag click
onFlagClick = (language, color) => {
this.changeLanguage(language);
this.changeColor(color);
}
render() {
return (
<div className="ui container">
<div>
Select a language:
<i
className="flag us"
onClick={() => this.onFlagClick("english","red")}
/>
<i
className="flag nl"
onClick={() => this.onFlagClick("dutch","blue")}
/>
</div>
<ColorContext.Provider value={this.state.color}> // highlight-line
<LanguageContext.Provider value={this.state.language}>
<UserCreate />
</LanguageContext.Provider>
</ColorContext.Provider> // highlight-line
</div>
);
}
}
export default App;
Then the tricky part comes in the Button
component, because the Context.Consumer
component needs to take a function as it's child, we want to break this into multiple pieces with a helper function.
import React, { Component } from "react";
import LanguageContext from "../contexts/LanguageContext";
import ColorContext from "../contexts/ColorContext";
class Button extends Component {
renderSubmit(value) {
return value === 'english' ? 'Submit' : 'Voorleggen';
}
renderButton(color) {
return (
<button className={`ui ${color} basic button`}>
<LanguageContext.Consumer>
{value => this.renderSubmit(value)}
</LanguageContext.Consumer>
</button>
)
}
render() {
return (
<ColorContext.Consumer>
{color => this.renderButton(color)}
</ColorContext.Consumer>
);
}
}
export default Button;
Does This Replace Redux
On short no. This context system does not replace Redux. Consider the following.
Context:
- Distributes data to various components
Redux:
- Distributes data to various components
- Centralizes data in a store
- Provides mechanism for changing data in the store
We will get into why it is not a complete replacement for Redux in the next post.
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.