by Diego Haz

React's new context API: toggle between local and global state


Consider a component that handles a visibility state and passes it down to its children via render props:

const PopoverContainer = () => (  <VisibilityContainer>    {({ toggle, hidden }) => (      <div>        <button onClick={toggle}>PopoverButton</button&gt;        <div hidden={hidden}>PopoverContent</div>      </div>    )}  </VisibilityContainer>);

What would you think about being able to make that state global by just changing a context property on the component?

const PopoverButton = () => (  <VisibilityContainer context="popover1">    {({ toggle }) => (      <button onClick={toggle}>PopoverButton</button>    )}  </VisibilityContainer>);
const PopoverContent = () => (  <VisibilityContainer context="popover1">    {({ hidden }) => (      &lt;div hidden={hidden}>PopoverContent</div>    )}  </VisibilityContainer>);

That's what we're going to achieve in this article.

Context and State

First, before talking about context and state in React, let me give you some context on the state of this topic (!).

Some months ago I published reas, an experimental UI toolkit powered by React and styled-components.

Besides components themselves, I wanted to provide helpers to handle their state. The approach I took at that time was to export some high-order components (HOCs), such as withPopoverContainer, so as to control the visibility state of a Popover component. Take a look at this example:

import { Popover, withPopoverContainer } from "reas";
const MyComponent = ({ toggle, visible }) => (  <div>    <button onClick={toggle}>Toggle</button>    <Popover visible={visible}>Popover</Popover>  </div>);
export default withPopoverContainer(MyComponent);

But HOCs have some problems, such as name collision. What if another HOC or a parent component passes its own toggle prop to MyComponent? Things will certainly break.

Even before that, inspired by Michael Jackson and his great talk, the React community started to adopt render props over HOCs.

Also, React v16.3.0 introduced a new context API, replacing the old unstable one, using render props.

I've learned to look at all that stuff that gets hyped up, especially the stuff brought up by the JavaScript community, with a critical eye. This keeps my mind sane and prevents me from having to refactor my code every single day with cool new libraries.

Finally, I posted a tweet asking people which they prefer: render props or HOCs. All comments were favorable to render props, which eventually made me turn all HOCs in reas into components with render props:

import { Popover } from "reas";
const MyComponent = () => (  <Popover.Container>    {({ toggle, visible }) => (      <div>        <button onClick={toggle}>Toggle&lt;/button&gt;        <Popover visible={visible}>Popover</Popover>      </div>    )}  </Popover.Container>);
export default MyComponent;

Popover.Container was a regular React component class with a toggle method using this.setState to change this.state.visible. Simple as that.

It was good and worked pretty well. However, in one of my projects I had a button that was supposed to control the Popover component placed in a completely different path in the React tree.

I either needed to have some sort of global state manager like Redux, or I needed to move Popover.Container up in the tree in a common parent and pass the props down until they touched both button and Popover . But this sounded like a terrible idea.

Also, setting up Redux and rewriting all the logic I already had with this.setState into actions and reducers just to have that functionality would be an awful job.

I think this imminent need of shared state is one of the reasons why people prematurely optimize their apps. That is, setting up all the libraries they might need up front, which includes a global state management library.

React's new context API comes in handy to solve this issue. I wanted to keep using regular React local state and only scale up to global state when needed, without needing to rewrite my state logic. That's why I built constate.



Let's see how PopoverContainer would look with constate:

import React from "react";import { Container } from "constate";
const PopoverContainer = props => (  <Container    initialState={{ visible: false }}    actions={{      toggle: () => state =>; ({ visible: !state.visible })    }}    {...props}  />);
export default PopoverContainer;

Now we can wrap our component with PopoverContainer so as to have access to visible and toggle members already passed by Container to the children function as an argument.

Also, note that we are passing all props received from PopoverContainer to Container. This means that we can compose it to create a new derived state component, such as AdvancedPopoverContainer, with new initialState and actions.

Under the hood

If you're like me, and you like to know how things were implemented under the hood, you're probably thinking about how Container was implemented. So, let's recreate a simple Container component:

import React from "react";
class Container extends React.Component {  state = this.props.initialState;
  render() {    return this.props.children({      ...this.state,      ...mapStateToActions(...)    });  }}
export default Container;

mapStateToActions is a utility function that passes state to each member of actions. That's what makes it possible to define our toggle function like this:

const actions = {  toggle: () =&gt; state => ({ visible: !state.visible})};

Our goal, however, is to be able to use the same PopoverContainer as a global state. With constate we just need to pass a context prop to Container:

<PopoverContainer context="popover1">  {({ toggle }) => (    <button onClick={toggle}>PopoverToggle</button>  )}</PopoverContainer>

Now, every Container with context="popover1" will share the same state.

Of course, you're curious about how Container handles that context prop. So here you go:

import React from "react";import Consumer from "./Consumer";
class Container extends React.Component {  state = this.props.initialState;
  render() {    if (this.props.context) {      return <Consumer {...this.props} />;    }
    return this.props.children({      ...this.state,      ...mapStateToActions(...)    });  }}
export default Container;

Ok, I'm sorry. Those four added lines don't tell you much. To create Consumer, we need to understand how to deal with the new React Context API.

React Context

We can break the new React Context API into three parts: Context, Provider and Consumer.

Let's create the context:

import React from "react";
const Context = React.createContext();
export default Context;

Then, we create our Provider, which uses Context.Provider and passes state and setState down:

import React from "react";import Context from "./Context";
class Provider extends React.Component {  handleSetState = fn => {    this.setState(state => ({      state: fn(state.state)    }));  };
  state = {    state: this.props.initialState,    setState: this.handleSetState  };
  render() {    return (      <Context.Provider value={this.state}>        {this.props.children}      </Context.Provider>    );  }}
export default Provider;

It can be a little tricky. We can't simply pass { state, setState } as a literal object to Context.Provider's value since it would recreate that object on every render. Learn more here.

Finally, our Consumer needs to use Context.Consumer to access state and setState passed by Provider:

import React from "react";import Context from "./Context";
const Consumer = ({ context, children, actions }) => (  <Context.Consumer>    {({ state, setState }) =&gt; children({      ...state[context],      ...mapContextToActions(...)    })}  </Context.Consumer>);
export default Consumer;

mapContextToActions is similar to mapStateToActions. The difference is that the former maps state[context] instead of just state.

The final step is to wrap our app with Provider:

import React from "react";import ReactDOM from "react-dom";import Provider from "./Provider";
const App = () => (  <Provider>    ...  &lt;/Provider>);
ReactDOM.render(<App />, document.getElementById("root"));

Finally, we have rewritten constate. Now you can use Container component to switch between local and global state with ease.


You might be thinking that starting a project with something like constate could also be a premature optimization. And you're probably right. You should stick with this.setState without abstractions as long as you can.

However, not all premature optimizations are the root of all evil. You should find a good balance between simplicity and scalability. That is, you should pursue simple implementations, specially if you're building small applications. But, if you're planning to grow, you should look for simple implementations that are also easy to scale.

Thank you for reading this!

If you like it and find it useful, here are some things you can do to show your support: