Codementor Events

Safe React Context::

Published Sep 16, 2018Last updated Mar 14, 2019
Safe React Context::

Something wicked this way comes

Context was initially created as means for third party library code to not have to know what you were doing with it while still granting you great power. Now it is in everyones hands. The new Context API React released at the beginning of this year was a great tool for React developers around the world and many experiments and new ways of moving data/state/actions/... have been created.

So, what can one do with Context? If we look at the React Docs this is what we get:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Sounds good and gets us thinking about passing props around in a nicer, less prop-drilly way (one prop going down a few or many levels before it is used). They also state the following:

Context is designed to share data that can be considered β€œglobal” for a tree of React components, such as the current authenticated user, theme, or preferred language.

That's all well and good. Take aways are:

  • There is a piece of data that I want shared across all my app (Global). Now this is what you will read everywhere that redux is great at doing 🧐
  • Shared across the tree of React componets. This is important. Context can be shared only within the tree it is created in. If you have multiple react roots in your app, you'll have to bridge across in a different way.

Melted into thin air

I'll be focusing on one of the advanced usecase examples exposed in the React Context docs, where methods to update your context are exposed through it (Updating Context from a nested component). In short, as a consumer I will have access to the context value and to a way to modify it:

// REACT DOCS EXAMPLE CODE

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // The Theme Toggler Button receives not only the theme
  // but also a toggleTheme function from the context
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

This is wonderful, right? A simple, declarative way to consume data and methods to modify it. Well it can be a bit of a double edge sword. If you notice, in this component I have no knowledge what so ever of what components live above me in the React tree. This means that if the Provider for the Context I want to consume doesn't exist I will consume it's default!!! πŸ€”

Is this so bad? Well, there are cases in which it is actually what you want but when you start creating a more complex Context container it might very well break you app (I've lived this unfortunate situation in prod).

Brevity is the soul of wit

This solution is for those situations when ensuring the context Provider lives about you is required 100% of the times. Also, it leans on a pattern that is becoming of general use. Wrapping the Consumer to trigger actions or ensure conditions.

So ... code code!!!

Break the ice

import React, { Fragment, createContext, Component } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const noop = Function.prototype;

// context.js
const defaultContext = {
  data: null,
  method: noop,
};

const { Provider, Consumer } = createContext(defaultContext);

// provider-container.js
class ProviderContainer extends Component {
  constructor(props) {
    super(props);

    this.state = {
      data: {},
      method: this.myMethod,
    };
  }

  myMethod = newData => {
    this.setState(state => ({
      data: {
        [newData]: newData
      } // Do things with your state
    }));
  };

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

// consumer-app.js

function App() {
  return (
    <Fragment>
      <Fragment>
        <h4>No Provider</h4>
        <Consumer>
          {({ data }) => {
            return data ? <div>DATA EXISTS</div> : <div>NO DATA</div>; // NO DATA :(
          }}
        </Consumer>
      </Fragment>

      <ProviderContainer>
        <h4>With Provider</h4>
        <Consumer>
          {({ data }) => {
            return data ? <div>DATA EXISTS</div> : <div>NO DATA</div>; // DATA :)
          }}
        </Consumer>
      </ProviderContainer>
    </Fragment>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

What you see here is the general use for Context:

  • A provider
  • A component wrapping the Provider to expose some state that can be modified
  • A consumer component/app using the Context Consumer

You can see in the consumer that if when our consumer tries to access the context with no Provider rendered above it no data will be available. Same applies to any method/s exposed. Let's enforce the presence of the Provider by throwing when it's not there.

Cruel to be kind

First, we will add a method/property on the context default and container to tell us if what we are consuming is in fact the default or not:

//  Modify the context default
const defaultContext = {
  data: null,
  method: noop,
  isDefault: () => true
};

// Modify the container state
...
    this.state = {
      data: {},
      method: this.myMethod,
      isDefault: () => false
    };
...

Now we will create a consumer wrapper, which will be the one we make available to components for consumption:

// context.js
const defaultContext = {
  data: null,
  method: noop,
  isDefault: () => true
};

const { Provider, Consumer: ReactConsumer } = createContext(defaultContext);

const Consumer = (props) => (
  <ReactConsumer>
    {contextValue => {
      if (contextValue.isDefault()) {
        throw new Error('Context Provider not found! In order to consume this context error free ensure you render a Provider in the ancestor tree')
      }

      return props.children(contextValue)
    }}
  </ReactConsumer>
)

export {
  Consumer,
  Provider
}

Now you can sleep tight knowing whoever uses this context will have to render the Provider above it or see their app throw a clearly visible error. Peace of mind thorugh a clear blocker πŸ˜‡

Foregone conclusion

If you find yourself create a Context that will not work properly when consumed with the default values, this is a nice way to expose it to your consumers. Better ideas and feeback are, as always, more than welcome.

Next post will be on 'Unwanted React Component Unmounting' and how to handle that beast.

Discover and read more posts from Fernando Bordallo
get started
post commentsBe the first to share your opinion
Show more replies