This guide demonstrates a React pattern for sharing global state, without requiring any techniques like ๐Ÿ˜’ Prop-Drilling, ๐Ÿ’ฒ MobX, or tools like ๐Ÿ€ Apollo Link State and ๐Ÿ”ฅ Redux.

The above emojis were auto-selected by Emoj.

The React Context API

Recently I found myself at an Advanced React Training with Michael Jackson (noโ€ฆ not the King of Pop, the King of React Router ๐Ÿ˜‰). We spent a good deal of the time working through Higher Order components and the new React Context API.

Being so new to The React Way, (yet so familiar with frameworks like Angular), I was surprised that React didnโ€™t ship with any built-in service architecture. React is a very different beast to other frameworks. It is intentionally designed to be all-state and no-service.

If you want to learn more about the React Context API, I recommend reading:

  1. The React Docs: The React Context API
  2. Blog: How to use the new React Context API

    I found this blog post easier to understand than the examples in the React documentation โ€” perhaps because Iโ€™m still on the React learning curve.

In my opinion, the fact that React is now shipping the Context API as a first-class citizen, means that a subscribing to global state (to use the term loosely), is no longer considered an anti-pattern. React is now providing a de-facto way of sharing state within the React tree without some of the limitations of higher order components.

But, the React Context API does not provide a method of dependency injection.

โ€œUnstatedโ€ Dependency Injection

Dependency injection would allow us to instantiate multiple copies of a component with a discrete state that can be provided and consumed at any point in the app.

This pattern is useful for:

  • Identical components that subscribe to different data sources with the same model, using the same methods and state properties
  • Testing snapshots with mock states

Unstated React Helper Logo

Thankfully, there is โ€œUnstatedโ€ โ€” a tiny dependency that provides a handy wrapper around the Context API for dependency injection. I want to encourage you to read the Unstated documentation and get a feel for what it does, as I will be using it in the code examples below: Unstated README.md.

Unstated Injection Demo

This CodeSandbox shows how to use Unstated to create multiple instantiations and inject them into separate providers of the same component.

Example: Creating a React Service with โ€œUnstatedโ€

So lets dig into things. My goal is make a reusable service module that I can inject into any part of the app. Our API Service must have the following requirements:

  1. Must be a self-contained re-usable module
  2. Can be Provided at any point in the React App
  3. Can be Subscribed to anywhere in the React App without need for prop-drilling

To keep things simple, we will mock out an API Service with the following methods and props.

  • boolean โ€˜loggedInโ€™
  • function โ€˜logIn()โ€™
  • function โ€˜logOut()โ€™

The Api.js module might look something like this:

/* Api.js */

import React from "react";
import { Provider, Subscribe, Container } from "unstated";

// Create a Container for our React Context. This container will
// hold state and methods just like a react component would:
export class ApiContainer extends Container {
  constructor() {
    super();

    // The state will be available to any component we inject
    // the Container instance into
    this.state = {
      loggedIn: false
    };
  }

  // These methods will also be avaiable anywhere we inject our
  // container context
  async login() {
    console.log("Logging in");
    this.setState({ loggedIn: true });
  }

  async logout() {
    console.log("Logging out");
    this.setState({ loggedIn: false });
  }
}

// Following the Singleton Service pattern (think Angular Service),
// we will instantiate the Container from within this module
const Api = new ApiContainer();

// Then we will wrap the provider and subscriber inside of functional
// React components. This simplifies the resuse of the module as we
// will be able to import this module as a depenency without having
// to import Unstated and/or create React Contexts  manually in the
// places that we want to Provide/Subscribe to the API Service.
export const ApiProvider = props => {
  // We leave the injector flexible, so you can inject a new dependency
  // at any time, eg: snapshot testing
  return <Provider inject={props.inject || [Api]}>{props.children}</Provider>;
};

export const ApiSubscribe = props => {
  // We also leave the subscribe "to" flexible, so you can have full
  // control over your subscripton from outside of the module
  return <Subscribe to={props.to || [Api]}>{props.children}</Subscribe>;
};

export default Api;

// IMPORT NOTE:
// With the above export structure, we have the ability to
// import like this:

// import Api, {ApiProvider, ApiSubscribe, ApiContainer}

// Api: Singleton Api instance, exported as default.
//      Contains your instantiated .state and methods.

// ApiProvider: Context Provider...
//      Publishes your React Context into the top of the
//      React App into the component tree.

// ApiSubscribe: Context Subsriber...
//      Subscribes to the higher Context from any place
//      lower than the point at which the Context was provided.

// ApiContainer:Context Container Class...
//      Used to instantiate new copy of your service if so desired.
//      Can be used for testing, or subsrcibing your class to a new
//      data source that uses the same data model/methods.

Now that we have our API service wrapped in module with Unstated, lets pull that into the top of our React App, inside of index.js:

/* index.js */

import React from "react";
import { render } from "react-dom";
import Routes from "./Routes";

// We can now import Api and unstated as one module
// This is our Api "Service".
import { ApiProvider } from "./Api";

// We can provide the API Context at the root of the React App
// with the <Api.Provier> component we created in the Api module
class App extends React.Component {
  render() {
    return (
      <div>
        <ApiProvider>
          <Routes />
        </ApiProvider>
      </div>
    );
  }
}

render(<App />, document.getElementById("root"));

The next piece of the puzzle is subscribing to the API Service from deep within the React App. To make our example a little more realistic, I am going to subscribe to the API service from down in the React components that get loaded by the router.

Lets subscribe to our API Service inside of Pages/Home.js:

/* pages/Home.js */

import React from "react";

// Import our Api Service Subscriber
import { ApiSubscribe } from "../Api";

const Home = () => {
  return (
    // Subscrube to the API Container instance. We can now pass
    // `api` into our component and use it's state and methods
    // without prop-drilling
    <ApiSubscribe>
      {api => (
        <div>
          <h1>๐Ÿ  Home</h1>
          <pre>
            api.state.loggedIn = {api.state.loggedIn ? "๐Ÿ‘ true" : "๐Ÿ‘Ž false"}
          </pre>
          <button onClick={() => api.login()}>Login</button>
          <button onClick={() => api.logout()}>Logout</button>
        </div>
      )}
    </ApiSubscribe>
  );
};

export default Home;

After adding a few more Routes and subscribing to the same API Container, we have our pattern set up.

Take the the pattern for a quick spin in this CodeSandbox below. Test the following:

  • Click Login: state.loggedIn prop gets updated in each route
  • Click Logout: state.loggedIn prop gets updated in each route
  • Click Login/Logout: from any route, the state is updated in all routes

Conclusion

Now we have a simple, re-usable React Service that uses the naturalized React Context API and the tiny Unstated module.

  • No prop drilling required
  • Dependency injection when needed

Iโ€™m going to call this the โ€œThe Unstated React Service Patternโ€ (unless another name already exists, or you have a better one ๐Ÿ˜€).

I am new to React. Most of the apps I have built in the last four years have been Angular or Vue.js. If you are an experienced React dev and you see a better way to do this, kindly share the โค๏ธ and show us your approach in the comments.

Always ready to learn.

       ___ _ _  ____________
 ___  | __/ | ||_   ___   _ \
/___/ | _|| | |__| | |_ \   /
      |_| |_|____|_||___/_|_\