Managing state has always been a little painful in modern web applications. With so many interdependent parts, having everything in sync can often turn into a big headache. Then React came along and simplified this problem elegantly. Setting the state within a component was as simple as calling the setState method. You could pass state down to child components easily.

The below should look familiar to those using React:

import React from 'react';

class Dashboard extends React.Component {
    constructor() {
        super();
        this.state = {
            username: "tom"
        }
    }

    render() {
        return (
             <Profile username={this.state.username} />
        );
    }
}

// ...

const Profile = props => <div>Hello {props.username}</div>;

We contain our username state in the Dashboard component, and pass it as a prop to our child component, Profile. Easy enough!

For simple applications, this works just fine. And if you can get away with managing all your state encapsulated within each component, you should absolutely continue to do this! However, for any reasonably sized project, you will soon run into the need to share state between components, often nested at varying depths in the component tree.

One option is simply to keep passing down props from parent to child to child to child to child etc. However, you quite often only need to use a particular prop in one component, with the ‘intermediary’ components simply passing the prop down. This is called ‘prop drilling’ – there’s nothing inherently wrong with this pattern, and it’s perfectly acceptable to use on small projects, but it becomes unwieldy on large ones.

You can also compose components to avoid overuse of the so-called ‘prop drilling’ pattern. Read more about this in the React documentation.

Wouldn’t it be great if we could simply store state at a ‘global’ level, and pull it into the component, at any depth, only when we need it?

Well, you can. At this point you’d normally reach for a state management package, such as Redux or MobX to handle this for you. Using Redux, you’d have a ‘store’ (essentially a global JS object that stores all the current state of your application), and you’d dispatch ‘actions’ to append some state onto the state history. You could dispatch actions from anywhere in the application, and access the store from anywhere using Redux’s connect method.

The trouble is, for some things Redux introduces a whole other level of complexity and an amount of code that feels out of balance with the feature you’re trying to implement. Is there a halfway house that allows simple state to be shared across disparate components, without a full blown Redux solution?

Context is everything

Under the hood, there has always been a shared state model in React. It’s called the Context API. Originally, this was just used for the internals of React and developers were discouraged from tinkering with it. However, it was used in some popular packages. Indeed, Redux itself is built on top of the Context API And in React 16.3 (Mar 2018), the Context API was released to the mainstream, which means you can safely use it now. Think of Context as an invisible layer/bus connecting all your components to a global state store. It solves a number of issues:

  • Avoids needing to prop drill through deeply nested components, by tapping into the context ‘layer’ only where you need to access global state.
  • No need for lots of boilerplate Redux (or similar), no complicated actions or reducers (although Redux is still useful if not essential for more complex builds)
  • A simple pattern of Providers and Consumers that will be familiar to newer and more junior developers (and god knows the state of frontend development needs some simplicity and sanity at the moment)

First things first, we need to create a Context that can be provided to and consumed by our components.

There is no ‘defined’ way to set up your project architecture to use Context, but the format I have used is to have a dedicated ‘contexts’ folder, containing a file for each context. We will only cover providing and consuming a single context in this post, but it is possible to have multiple contexts in your application, and having them separated will keep things organised.

// contexts/MyContext.js

import React from 'react';

export const MyContext = React.createContext({
    username: "",
    changeName: () => {}
});

createContext() accepts an object that defines the ‘shape’ of the Context and its default values, as you can see above. This is optional. You do not need to provide default values and you can just use createContext(), but it provides a type-safe fallback. We’ve assigned it to a constant called MyContext.

That’s it. No, seriously – that’s it. You now have a Context that can be imported throughout your application!

Providers

Providers are the wrappers that allow your components to inherit the context and the application state. You can choose to provide your context to a limited part of the application, but most often you’ll just want to provide it to the whole application. We can write a small wrapper that exposes the state to all child components, to make things more manageable.

Back to our contexts/MyContext.js file:

// contexts/MyContext.js

import React from 'react';

export const MyContext = React.createContext({
    username: "",
    changeName: () => {}
});

export class MyProvider extends React.Component {
    constructor(props) {
        super(props);

        this.changeName = () => {
            this.setState({
                username: this.state.username === "Tom" ? "Dave" : "Tom"
            });
        };

        this.state = {
            username: "Tom",
            triggerChangeName: this.changeName
        };
    }

    render() {
        return (
            <MyContext.Provider value={this.state}>
                {this.props.children}
            </MyContext.Provider>
        );
    }
}

Here we’ve set up a Provider, and wrapped it in a class called MyProvider. This is just one pattern, you don’t need to create a wrapper – you can access Providers directly (but there are significant performance improvements by creating a wrapper that only has uses this.props.children). We’ve set the username to “Tom” and the triggerChangeName function is something we will use later to pass a change to the global state back into Context.

Let’s now wrap our Application in the Provider so we can expose the Context to whatever components wish to access it.

Now, open App.js (or wherever you want to insert the Provider):

// App.js

// ...

import {MyProvider} from 'contexts/AnimationContext';

// ...

render() {
    return (
        <MyProvider>
            <Header />
            <Content />
            <Footer />
        </MyProvider>
    );
}

Now all the child components of MyProvider can import and have access to the Context.

If we wanted to use the context in a deeply nested Component, we can then simply import the Context we’ve created, and use a Consumer.

Consumers

Consumers are how we take context and make it available to use. Here we have created a new component called UserProfile. It imports our Context, and has a Consumer (MyContext.Consumer) to use some values stored in the Context. We then pass in our username state value, and our triggerChangeName function.

// UserProfile.js

import React from 'react';
import {MyContext} from 'contexts/MyContext';

export default class UserProfile extends React.Component {
    render() {
        return (
            <MyContext.Consumer>
                {({username, triggerChangeName}) => (
                    <div>
                        Hi {username}!
                        <button onClick={triggerChangeName}>Change Username</button>
                    </div>
                )}
            </MyContext.Consumer>
        );
    }
}

This is a class component, but could just as well be a stateless function component.

And hey presto – we have accessed the username from our Context, without having to pass that prop through layers of other components! And even better, our onClick button will send the triggerChangeName event via Context and execute that function (in this case, toggling the state value for username - see the original Provider).

No intermediary components needed to know about these values or functions, and no props were drilled down. Only the components that needed the values accessed them. Nice and clean!

Functions go First Class

Remember, functions are ‘first class’ in JavaScript, meaning they can be passed as arguments, functions can return functions, and so on.

This means you aren’t limited to using Context within render functions, and you can take it inside event handlers and lifecycle hooks. Ryan Florence explains this is great detail in this post, and it’s a very useful pattern to know. I recently used this pattern to pass a context function as an argument in a React Router Link onClick function. Here’s a snippet:

import React from 'react';
import {AnimationContext} from 'contexts/AnimationContext';

// ...

nextSection(e, location, triggerWipe) {
    e.preventDefault();
    triggerWipe();

    setTimeout(() => {
        this.props.history.push(location);
    }, 1500);

    this.storeQuizResult(quizResult, selectedAnswers);
}

// ...

<AnimationContext.Consumer>
    {({triggerWipe}) => (
        <Link
            onClick={(e) => this.nextSection(e, location, triggerWipe)}
            to={location}
        >
            Start
        </Link>
    )}
</AnimationContext.Consumer>

// ...

Wrapping Up

There is a lot more to Context than I can cover here, but luckily there are a number of great references out there to help you. It’s also fairly new functionality within React, so things might change, and shouldn’t be considered a replacement for Redux just yet. But for simple state management without the overhead, Context is a great tool. Have a play around in CodeSandbox.