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.

components/App.js
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.

toggle language on click

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.

components/UserCreate.js
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;
components/Field.js
import React, { Component } from "react";

class Field extends Component {
  render() {
    return (
      <div className="ui field">
        <label> Name </label>
        <input />
      </div>
    );
  }
}

export default Field;
components/Button.js
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.

field and submit button

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.

contexts/LanguageContext.js
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.

contexts/LanguageContext.js
import React from 'react';

export default React.createContext('english');

Get Data Out

Then to reference this context we add the following to the child component (in this case Button)

components/Button.js
import React, { Component } from "react";

import LanguageContext from "../contexts/LanguageContext";
class Button extends Component {
  static contextType = LanguageContext;
  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.

components/Button.js
import React, { Component } from "react";

import LanguageContext from "../contexts/LanguageContext";
class Button extends Component {

  render() {
    return <button className="ui button primary"> Submit</button>;
  }
}

Button.contextType = LanguageContext;
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.

components/Button.js
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';
    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.

components/App.js
import React, { Component } from "react";

import UserCreate from "./UserCreate";

import LanguageContext from "../contexts/LanguageContext";
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} >          <UserCreate />
        </LanguageContext.Provider>      </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.

components are updating

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.

components/Button.js
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.

components/App.js
import React, { Component } from "react";

import UserCreate from "./UserCreate";

import LanguageContext from "../contexts/LanguageContext";
import ColorContext from "../contexts/ColorContext";
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}>          <LanguageContext.Provider value={this.state.language}>
            <UserCreate />
          </LanguageContext.Provider>
        </ColorContext.Provider>      </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.

components/Button.js
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;

button text and color changing on flag click

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.