Web front-end
5 minute read

Working with the React Context API

Boris is a web developer working primarily with vanilla JavaScript and popular frameworks like Angular, React, and MeteorJS.

The React Context API has been around as an experimental feature for a while now, but only in React’s version 16.3.0 did it become safe to use in production. The article below will show you two basic web store apps, one built with the Context API and one without it.

This new API solves one major problem–prop drilling. Even if you’re not familiar with the term, if you’ve worked on a React.js app, it has probably happened to you. Prop drilling is the processing of getting data from component A to component Z by passing it through multiple layers of intermediary React components. Component will receive props indirectly and you, the React Developer will have to ensure everything works out right.

Let’s explore how you would handle common problems without the React Context API,

App.js

class App extends Component {
    state = {
        cars: {
            car001: { name: 'Honda', price: 100 },
            car002: { name: 'BMW', price: 150 },
            car003: { name: 'Mercedes', price: 200 }
        }
    };
    incrementCarPrice = this.incrementCarPrice.bind(this);
    decrementCarPrice = this.decrementCarPrice.bind(this);

    incrementCarPrice(selectedID) {
        // a simple method that manipulates the state
        const cars = Object.assign({}, this.state.cars);
        cars[selectedID].price = cars[selectedID].price + 1;
        this.setState({
            cars
        });
    }

    decrementCarPrice(selectedID) {
        // a simple method that manipulates the state
        const cars = Object.assign({}, this.state.cars);
        cars[selectedID].price = cars[selectedID].price - 1;
        this.setState({
            cars
        });
    }

    render() {
        return (
            <div className="App">
                <header className="App-header">
                    <img src={logo} className="App-logo" alt="logo" />
                    <h1 className="App-title">Welcome to my web store</h1>
                </header>
                {/* Pass props twice */}
                <ProductList
                    cars={this.state.cars}
                    incrementCarPrice={this.incrementCarPrice}
                    decrementCarPrice={this.decrementCarPrice}
                />
            </div>
        );
    }
}

ProductList .js

const ProductList = props => (
    <div className="product-list">
        <h2>Product list:</h2>
        {/* Pass props twice */}
        <Cars
            cars={props.cars}
            incrementCarPrice={props.incrementCarPrice}
            decrementCarPrice={props.decrementCarPrice}
        />
        {/* Other potential product categories which we will skip for this demo: */}
        {/* <Electronics /> */}
        {/* <Clothes /> */}
        {/* <Shoes /> */}
    </div>
);

export default ProductList;

Cars.js

const Cars = props => (
    <Fragment>
        <h4>Cars:</h4>
        {/* Finally we can use data */}
        {Object.keys(props.cars).map(carID => (
            <Car
                key={carID}
                name={props.cars[carID].name}
                price={props.cars[carID].price}
                incrementPrice={() => props.incrementCarPrice(carID)}
                decrementPrice={() => props.decrementCarPrice(carID)}
            />
        ))}
    </Fragment>
);

Car.js

const Cars = props => (
    <Fragment>
        <p>Name: {props.name}</p>
        <p>Price: ${props.price}</p>
        <button onClick={props.incrementPrice}>&uarr;</button>
        <button onClick={props.decrementPrice}>&darr;</button>
    </Fragment>
);

Granted, this isn’t the best way to handle your data, but I hope it demonstrates why prop drilling sucks. So how can the Context API help us avoid this?

Introducing the Context Web Store

Let’s refactor the app and demonstrate what it can do. In a few words, the Context API allows you to have a central store where your data lives (yes, just like in Redux). The store can be inserted into any component directly. You can cut out the middleman!

Example of two state flows: one with the React Context API, and one without

The refactoring is quite easy–we don’t have to make any changes to how the components are structured. We do need to create some new components, however–a provider and a consumer.

1. Initialize the Context

First, we need to create the context, which we can later use to create providers and consumers.

MyContext.js

import React from 'react';

// this is the equivalent to the createStore method of Redux
// https://redux.js.org/api/createstore

const MyContext = React.createContext();

export default MyContext;

2. Create the Provider

Once that’s done, we can import the context and use it to create our provider, which we’re calling MyProvider. In it, we initialize a state with some values, which you can share via value prop our provider component. In our example, we’re sharing this.state.cars along with a couple of methods that manipulate the state. Think of these methods as reducers in Redux.

MyProvider.js

import MyContext from './MyContext';

class MyProvider extends Component {
    state = {
        cars: {
            car001: { name: 'Honda', price: 100 },
            car002: { name: 'BMW', price: 150 },
            car003: { name: 'Mercedes', price: 200 }
        }
    };

    render() {
        return (
            <MyContext.Provider
                value={{
                    cars: this.state.cars,
                    incrementPrice: selectedID => {
                        const cars = Object.assign({}, this.state.cars);
                        cars[selectedID].price = cars[selectedID].price + 1;
                        this.setState({
                            cars
                        });
                    },
                    decrementPrice: selectedID => {
                        const cars = Object.assign({}, this.state.cars);
                        cars[selectedID].price = cars[selectedID].price - 1;
                        this.setState({
                            cars
                        });
                    }
                }}
            >
                {this.props.children}
            </MyContext.Provider>
        );
    }
}

To make the provider accessible to other components, we need to wrap our app with it (yes, just like in Redux). While we’re at it, we can get rid of the state and the methods because they are now defined in MyProvider.js.

App.js

class App extends Component {
    render() {
        return (
            <MyProvider>
                <div className="App">
                    <header className="App-header">
                        <img src={logo} className="App-logo" alt="logo" />
                        <h1 className="App-title">Welcome to my web store</h1>
                    </header>
                    <ProductList />
                </div>
            </MyProvider>
        );
    }
}

3. Create the Consumer

We’ll need to import the context again and wrap our component with it which injects the context argument in the component. Afterward, it’s pretty straight forward. You use context, the same way you would use props. It holds all the values we’ve shared in MyProducer, we just need to use it!

Cars.js

const Cars = () => (
    <MyContext.Consumer>
        {context => (
            <Fragment>
                <h4>Cars:</h4>
                {Object.keys(context.cars).map(carID => (
                    <Car
                        key={carID}
                        name={context.cars[carID].name}
                        price={context.cars[carID].price}
                        incrementPrice={() => context.incrementPrice(carID)}
                        decrementPrice={() => context.decrementPrice(carID)}
                    />
                ))}
            </Fragment>
        )}
    </MyContext.Consumer>
);

What did we forget? The ProductList! This is where the benefit becomes apparent. We don’t pass any data or methods. The component is simplified because it only needs to render a few components.

ProductList.js

const ProductList = () => (
    <div className="product-list">
        <h2>Product list:</h2>
        <Cars />
        {/* Other potential product categories which we will skip for this demo: */}
        {/* <Electronics /> */}
        {/* <Clothes /> */}
        {/* <Shoes /> */}
    </div>
);

Over the course of this article, I made a few comparisons between Redux and the Context API. One of the biggest advantages of Redux is that fact that your app can have a central store which can be accessed from any component. With the new Context API, you have that functionality by default. A lot of hype has been made that the Context API will render Redux obsolete.

This might be true for those of you that only use Redux for its central store capabilities. If that’s the only feature you were using it for you can now replace it with the Context API and avoid prop drilling without using third-party libraries.

If you’re interesting in measuring and optimizing the performance of your React application, read A Guide to Optimizing React Performance by fellow Toptaler William Wang.

Understanding the basics

What is a consumer?

A consumer is where the stored information ends up. It can request data via the provider and manipulate the central store if the provider allows it.

Is ReactJS a front-end or back-end library?

React is a library for building user interfaces on the client, so it is mainly used on the front end of an application. However, to improve performance and to help with SEO, React applications have the ability to be rendered on the back end.

What is a component in ReactJS?

Static websites tend to divide themselves into pages that may share UI with other pages or be completely different. ReactJS applications are split into components (e.g. dropdown, embedded video, carousel). This helps save time because developers only need to write a component once and can reuse it on any page.

What is a component’s state in ReactJS?

A React component can have its own state, which holds information about the component’s current configuration. For example, a user registration form might store whether or not the user has checked the "Terms and Conditions" checkbox in its state.

Can you share a component’s state?

Developers can share a component's state by passing it to child components (ones that are rendered by the parent) via their props. In a user registration form, you can disable the submit button until the user has accepted the "Terms and Conditions." The form can share the checkbox’ state via the button’s props.

Can you share a component’s state with a parent or sibling component in React?

No, you can’t. In React, data flows from the root level parents to the most deeply nested child. This is why it can be necessary to keep a central store of data and access it where you need it instead of endlessly passing it from parent to child.

Are React props immutable?

React props are immutable. A component can only inherit them, but not modify them. If they do need to be modified, this can only happen by “asking” the parent component to do so. This can be done by using a callback or a hook.

Does the React Context API make Redux obsolete?

The Context API makes one feature of Redux obsolete - the central store. If you don't use any of Redux's other features you can replace the whole library with this new native (to React) solution.

What is context in React?

React's context allows you to share information to any component, by storing it in a central place and allowing access to any component that requests it (usually you are only able to pass data from parent to child via props).

What is a provider?

The provider acts as a delivery service. When a consumer asks for something, it finds it in the context and delivers it to where it's needed.