Putting Things in Context With React

Avatar of Neal Fennimore
Neal Fennimore on (Updated on )

UGURUS offers elite coaching and mentorship for agency owners looking to grow. Start with the free Agency Accelerator today.

Context is currently an experimental API for React – but soon to be a first class citizen! There are a lot of reasons it is interesting but perhaps the most is that it allows for parent components to pass data implicitly to their children, no matter how deep the component tree is. In other words, data can be added to a parent component and then any child can tap into it.

See the Pen React Context Lights by Neal Fennimore (@nealfennimore) on CodePen.

While this is often the use case for using something like Redux, it’s nice to use if you do not need complex data management. Think about that! We create a custom downstream of data, deciding which props are passed and at which levels. Pretty cool.

Context is great in areas of where you have a lot of components that depend on a single piece of data, but are deep within the component tree. Explicitly passing each prop to each individual component can often be overwhelming and it is a lot easier just to use context here.

For example, let’s consider how we would normally pass props down the tree. In this case, we’re passing the color red using props on each component in order to move it on down the stream.

class Parent extends React.Component {
  render(){
    return <Child color="red" />;
  }
}

class Child extends React.Component {
  render(){
    return <GrandChild color={this.props.color} />
  }
}

class GrandChild extends React.Component {
  render(){
    return (
      <div style={{color: this.props.color}}>
        Yep, I'm the GrandChild
      </div>
    );
  }
}

What if we never wanted the Child component to have the prop in the first place? Context saves us having to go through the Child component with color and pass it directly from the Parent to the GrandChild:

class Parent extends React.Component {
  // Allow children to use context
  getChildContext() {
    return {
      color: 'red'
    };
  }
  
  render(){
    return <Child />;
  }
}

Parent.childContextTypes = {
  color: PropTypes.string
};

class Child extends React.Component {
  render() {
    // Props is removed and context flows through to GrandChild
    return <GrandChild />
  }
}

class GrandChild extends React.Component {
  render() {
    return (
      <div style={{color: this.context.color}}>
        Yep, I'm still the GrandChild
      </div>
    );
  }
}

// Expose color to the GrandChild
GrandChild.contextTypes = {
  color: PropTypes.string
};

While slightly more verbose, the upside is exposing the color anywhere down in the component tree. Well, sometimes…

There’s Some Gotchas

You can’t always have your cake and eat it too, and context in it’s current form is no exception. There are a few underlying issues that you’ll more than likely come into contact with, if you end up using context for all but the simplest cases.

Context is great for being used on an initial render. Updating context on the fly? Not so much. A common issue with context is that context changes are not always reflected in a component.

Let’s dissect these gotchas in more detail.

Gotcha 1: Using Pure Components

Context is hard when using PureComponent, since by default it does not perform any shallow diffing with context. Shallow diffing with PureComponent is testing for whether the values of the object are strictly equal. If they’re not, then (and only then) will the component update. But since context is not checked, well… nothing happens.

See the Pen React Context Lights with PureComponents by Neal Fennimore (@nealfennimore) on CodePen.

Gotcha 2: Should Component Update? Maybe.

Context also does not update if a component’s shouldComponentUpdate returns false. If you have a custom shouldComponentUpdate method, then you’ll also need to take context into consideration. To enable updates with context, we could update each individual component with a custom shouldComponentUpdate that looks something like this.

import shallowEqual from 'fbjs/lib/shallowEqual';

class ComponentThatNeedsColorContext extends React.PureComponent {
  // nextContext will show color as soon as we apply ComponentThatNeedsColorContext.contextTypes
  // NOTE: Doing the below will show a console error come react v16.1.1
  shouldComponentUpdate(nextProps, nextState, nextContext){
    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || !shallowEqual(this.context, nextContext);
  }
}

ComponentThatNeedsColorContext.contextTypes = {
  color: PropTypes.string
};

However, this does not solve the issue of an intermediary PureComponent between the parent and the child blocking context updates. This means that every PureComponent between the parent and child would need to have contextTypes defined on it, and they would also need to have an updated shouldComponentUpdate method. And at this point, that’s a lot of work for very little gain.

Better Approaches to the Gotchas

Fortunately, we have some ways to work around the gotchas.

Approach 1: Use a Higher Order Component

A Higher Order Component can read from context and pass the needed values on to the next component as a prop.

import React from 'react';

const withColor = (WrappedComponent) => {
    class ColorHOC extends React.Component {
        render() {
            const { color } = this.context;        
            return <WrappedComponent style={{color: color}} {...this.props} />
        }
    }
         
    ColorHOC.contextTypes = {
        color: React.PropTypes.string  
    };

    return ColorHOC;
};


export const Button = (props)=> <button {...props}>Button</button>

// ColoredButton will render with whatever color is currently in context with a style prop
export const ColoredButton = withColor( Button );

See the Pen React Context Lights with HOC by Neal Fennimore (@nealfennimore) on CodePen.

Approach 2: Use Render Props

Render Props allow us to use props to share code between two components.

class App extends React.Component {
    getChildContext() {
        return {
            color: 'red'
        }
    }

    render() {
        return <Button />
    }
}

App.childContextTypes = {
    color: React.PropTypes.string
}

// Hook 'Color' into 'App' context
class Color extends React.Component {
    render() {
        return this.props.render(this.context.color);
    }
}

Color.contextTypes = {
    color: React.PropTypes.string
}

class Button extends React.Component {
    render() {
        return (
            <button type="button">
                {/* Return colored text within Button */}
                <Color render={ color => (
                    <Text color={color} text="Button Text" />
                ) } />
            </button>
        )
    }
}

class Text extends React.Component {
    render(){
        return (
            <span style={{color: this.props.color}}>
                {this.props.text}
            </span>
        )
    }
}

Text.propTypes = {
    text: React.PropTypes.string,
    color: React.PropTypes.string,
}

Approach 3: Dependency Injection

A third way we can work around these gotchas is to use Dependency Injection to limit the context API and allow components to subscribe as needed.

The New Context

The new way of using context, which is currently slated for the next minor release of React (16.3), has the benefits of being more readable and easier to write without the “gotchas” from previous versions. We now have a new method called createContext, which defines a new context and returns both a Provider and Consumer.

The Provider establishes a context that all sub-components can hook into. It’s hooked in via Consumer which uses a render prop. The first argument of that render prop function, is the value which we have given to the Provider. By updating the value within the Provider, all consumers will update to reflect the new value.

As a side benefit with using the new context, we no longer have to use childContextTypes, getChildContext, and contextTypes.

const ColorContext = React.createContext('color');
class ColorProvider extends React.Component {
    render(){
        return (
            <ColorContext.Provider value={'red'}>
                { this.props.children }
            </ColorContext.Provider>
        )
    }
}

class Parent extends React.Component {  
    render(){
        // Wrap 'Child' with our color provider
        return (
            <ColorProvider>
                <Child />
            </ColorProvider>
        );
    }
}

class Child extends React.Component {
    render(){
        return <GrandChild />
    }
}

class GrandChild extends React.Component {
    render(){
        // Consume our context and pass the color into the style attribute
        return (
            <ColorContext.Consumer>
                {/* 'color' is the value from our Provider */}
                {
                    color => (
                        <div style={{color: color}}>
                            Yep, I'm still the GrandChild
                        </div>
                    )
                }
            </ColorContext.Consumer>
        );
    }
}

Separate Contexts

Since we have more granular control in how we expose context and to what components are allowed to use it, we can individually wrap components with different contexts, even if they live within the same component. We can see this in the next example, whereby using the LightProvider twice, we can give two components a separate context.

See the Pen React Context Lights with new Context by Neal Fennimore (@nealfennimore) on CodePen.

Conclusion

Context is a powerful API, but it’s also very easy to use incorrectly. There are also a few caveats to using it, and it can be very hard to figure out issues when components go awry. While Higher-Order Components and dependency injection offer alternatives for most cases, context can be used beneficially in isolated portions of your code base.

With the next context though, we no longer have to worry about the gotchas we had with the previous version. It removes having to define contextTypes on individual components and opens up the potential for defining new contexts in a reusable manner.