23/09/2018, 00:39

The React Context API

Despite having debuted before but under the disguise of a behind-the-scene features most developers either knew little about, or avoided using because the documents said so, the React Context API has evoluted into a first-class citizen in React 16.3 recently, open to all. As soon as React 16.3 ...

Despite having debuted before but under the disguise of a behind-the-scene features most developers either knew little about, or avoided using because the documents said so, the React Context API has evoluted into a first-class citizen in React 16.3 recently, open to all.

As soon as React 16.3 was released, there were articles all across the Internet proclaiming the death of Redux because of this new Context API. If Redux could communicate verbally, its human form manifestation would say “Hey, the reports of my death are just exaggerations.”

Today, in this article, I would like to cover as much as possible the following things:

  • How the new Context API works?
  • How it is similar to Redux?
  • When you might prefer the Context rather than using Redux?
  • Why Context doesn't "steal the show" of Redux in every scene.

Suppose that we need to build an application that displays the user's information after successful authentication. The info is distributed across 2 places: in the nav bar at the top-right, and in the sidebar on the leftside of the main content.

With pure React, the keeper of user info must be high enough in the hierarchy so that the data can be passed down as props attribute to the children components that need it, which, in this case, is App:

  1. App passes it along to Nav and Body.
  2. They, in turn, pass it down again, to UserAvatar and Sidebar.
  3. Finally, Sidebar passes it down to UserInfo.
import React from 'react';
import ReactDOM from 'react-dom';

const UserAvatar = ({ user, size }) => (
  <img
    className={`user-avatar ${size || '}`}
    alt="user avatar"
    src={user.avatar}
  />
);

const UserInfo = ({ user }) => (
  <div className="user-info">
    <div>
      <UserAvatar user={user} />
      {user.name}
    </div>
    <div className="stats">
      <div>{user.followers} Followers</div>
      <div>Following {user.following}</div>
    </div>
  </div>
);

const Nav = ({ user }) => (
  <div className="nav">
    <UserAvatar user={user} size="small" />
  </div>
);

const Content = () => <div className="content">main content here</div>;

const Sidebar = ({ user }) => (
  <div className="sidebar">
    <UserInfo user={user} />
  </div>
);

const Body = ({ user }) => (
  <div className="body">
    <Sidebar user={user} />
    <Content user={user} />
  </div>
);

class App extends React.Component {
  state = {
    user: {
      avatar: 'https://lh3.googleusercontent.com/-4rElFw0sdsY/AAAAAAAAAAI/AAAAAAAAAA8/TieQ-Ya9NLQ/s640-il/photo.jpg',
      name: 'Jack Frost'
      followers: 1234,
      following: 123
    }
  };

  render() {
    const { user } = this.state;

    return (
      <div className="app">
        <Nav user={user} />
        <Body user={user} />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector('#root'));

In terms of prop drilling, this is not a terrible workaround. Prop drilling is not discouraged by any means; it’s a perfectly valid pattern and core to the way React works. But deep drilling might be a bit annoying to write and extremely annoying when you have a lot of props to pass down.

Furthermore, prop drilling strategy has one colossal downside: it creates coupling between components that would otherwise be decoupled. In the example above, Nav needs to accept a user prop and pass it down to UserAvatar, even though it has no need for the user. Such tightly-coupled components have no reusability, because you’ve gotta wire them up with their new parents whenever you plop one down in a new location.

Still sticking with pure React, the children prop is a better solution for components serving as generic placeholders like Nav, Sidebar, and Body. Just so you know, you can pass JSX elements into any prop and render them as-is. No more worry about passing down specific pieces of data that the component needs.

import React from 'react';
import ReactDOM from 'react-dom';
import "./styles.css";

const UserAvatar = ({ user, size }) => (
  <img
    className={`user-avatar ${size || '}`}
    alt="user avatar"
    src={user.avatar}
  />
);

const UserInfo = ({ user }) => (
  <div className="user-info">
    <div>
      <UserAvatar user={user} />
      {user.name}
    </div>
    <div className="stats">
      <div>{user.followers} Followers</div>
      <div>Following {user.following}</div>
    </div>
  </div>
);

// Accept children and render it/them
const Nav = ({ children }) => (
  <div className="nav">
    {children}
  </div>
);

const Content = () => (
  <div className="content">main content here</div>
);

const Sidebar = ({ children }) => (
  <div className="sidebar">
    {children}
  </div>
);

// Body needs a sidebar and content, but written this way,
// they can be ANYTHING
const Body = ({ sidebar, content }) => (
  <div className="body">
    <Sidebar>{sidebar}</Sidebar>
    {content}
  </div>
);

class App extends React.Component {
  state = {
    user: {
      avatar:
        'https://lh3.googleusercontent.com/-4rElFw0sdsY/AAAAAAAAAAI/AAAAAAAAAA8/TieQ-Ya9NLQ/s640-il/photo.jpg',
      name: 'Jack Frost'
      followers: 1234,
      following: 123
    }
  };

  render() {
    const { user } = this.state;

    return (
      <div className="app">
        <Nav>
          <UserAvatar user={user} size="small" />
        </Nav>
        <Body
          sidebar={<UserInfo user={user} />}
          content={<Content />}
        />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector('#root'));

Nevertheless, it will be tough to figure out how to adapt the children pattern if your app is too complex so let's see how you might replace the prop drilling with Redux in the following section.

Here’s the React app from above, refactored to use Redux. The user info has been moved to the Redux store, which means we can use react-redux’s connect function to directly inject the user prop into any components.

This is a big win in terms of decoupling. Take a look at the new Nav, Body, and Sidebar, which no loger accept and pass down the user prop. No more playing hot potato with props. No more needless coupling.

import React from 'react';
import ReactDOM from 'react-dom';

// We need createStore, connect, and Provider:
import { createStore } from "redux";
import { connect, Provider } from "react-redux";

// Create a reducer with an empty initial state
const initialState = {};
function reducer(state = initialState, action) {
  switch (action.type) {
    // Respond to the SET_USER action and update
    // the state accordingly
    case 'SET_USER':
      return {
        ...state,
        user: action.user
      };
    default:
      return state;
  }
}

// Create the store with the reducer
const store = createStore(reducer);

// Dispatch an action to set the user
// (since initial state is empty)
store.dispatch({
  type: 'SET_USER',
  user: {
    avatar: 'https://lh3.googleusercontent.com/-4rElFw0sdsY/AAAAAAAAAAI/AAAAAAAAAA8/TieQ-Ya9NLQ/s640-il/photo.jpg',
    name: 'Jack Frost'
    followers: 1234,
    following: 123
  }
});

// This mapStateToProps function extracts a single
// key from state (user) and passes it as the `user` prop
const mapStateToProps = state => ({
  user: state.user
});

// connect() UserAvatar so it receives the `user` directly,
// without having to receive it from a component above

// could also split this up into 2 variables:
//   const UserAvatarAtom = ({ user, size }) => ( ... )
//   const UserAvatar = connect(mapStateToProps)(UserAvatarAtom);
const UserAvatar = connect(mapStateToProps)(({ user, size }) => (
  <img
    className={`user-avatar ${size || '}`}
    alt="user avatar"
    src={user.avatar}
  />
));

// connect() UserInfo so it receives the `user` directly,
// without having to receive it from a component above
// (both use the same mapStateToProps function)
const UserInfo = connect(mapStateToProps)(({ user }) => (
  <div className="user-info">
    <div>
      <UserAvatar />
      {user.name}
    </div>
    <div className="stats">
      <div>{user.followers} Followers</div>
      <div>Following {user.following}</div>
    </div>
  </div>
));

// Nav doesn't need to know about `user` anymore
const Nav = () => (
  <div className="nav">
    <UserAvatar size="small" />
  </div>
);

const Content = () => (
  <div className="content">main content here</div>
);

// Sidebar doesn't need to know about `user` anymore
const Sidebar = () => (
  <div className="sidebar">
    <UserInfo />
  </div>
);

// Body doesn't need to know about `user` anymore
const Body = () => (
  <div className="body">
    <Sidebar />
    <Content />
  </div>
);

// App doesn't hold state anymore, so it can be
// a stateless function
const App = () => (
  <div className="app">
    <Nav />
    <Body />
  </div>
);

// Wrap the whole app in Provider so that connect()
// has access to the store
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector('#root')
);

Although curiosity is not intrinsically good, it's a good thing to wonder how Redux achieves this magic trick. How come Redux is able to do it while React is unable to overcome the limitation of supporting passing porps down multiple levels?

The answer is, Redux does use React’s context feature. Not the modern Context API (not yet) – the old one, which the documents said not to use unless you were writing a library or knew what you were doing. Context is like an electrical bus running behind every component: to receive the power (data) passing through it, you need only plug in. And (React-)Redux’s connect function does just that.

This feature of Redux is just the tip of the iceberg, though. Passing data around all over the place is just the most apparent of Redux’s features. Here are a few other benefits you get out of the box:

Automatic performance improvement

connect automatically makes connected components “pure,” meaning they will only re-render when their props change – a.k.a. when their slice of the Redux state changes. This prevents needless re-renders and keeps your app running fast.

Easy Debugging

Another great feature the Redux DevTools enable is time travel debugging a.k.a. you can click on any past action and jump to that point in time, basically replaying every action up to and including that one (but no further).

Middleware Customization Support

Redux supports the concept of middleware, which is a fancy alias for “a function that runs every time an action is dispatched”. You can customize your own middleware as it isn't difficult as it seems and Redux middlewares often enable powerful stuff.

But there might be a day that you don't need all those fancy features of Redux. I mean, why do I have to care about the easy debugging, the middleware cutomization or the automatic performance improvements while all I want to do it just pass data around easily and get the job done.

React’s new Context API will probably fit the bill. Let’s see how it works.

5.1. Implement Context API

There are 3 important pieces to the context API:

The React.createContext function, which creates the context

The Provider, which establishes the “electrical bus” running through a component tree. The Provider is very similar in its nature in comparison with React-redux's Provider. It takes the form of an object containing your data and any actions you want to be able to perform on the data.

The Consumer, which taps into the “electrical bus” to extract the data. The Consumer functions a little bit like React-Redux’s connect function, tapping into the data and making it available to the components.

// Up top, we create a new context
// This is an object with 2 properties: { Provider, Consumer }
// Note that it's named with UpperCase, not camelCase
// This is important because we'll use it as a component later
// and Component Names must start with a Capital Letter
const UserContext = React.createContext();

// Components that need the data tap into the context
// by using its Consumer property. Consumer uses the
// "render props" pattern.
const UserAvatar = ({ size }) => (
  <UserContext.Consumer>
    {user => (
      <img
        className={`user-avatar ${size || '}`}
        alt="user avatar"
        src={user.avatar}
      />
    )}
  </UserContext.Consumer>
);

// Notice that we don't need the 'user' prop any more,
// because the Consumer fetches it from context
const UserInfo = () => (
  <UserContext.Consumer>
    {user => (
      <div className="user-info">
        <div>
          <UserAvatar user={user} />
          {user.name}
        </div>
        <div className="stats">
          <div>{user.followers} Followers</div>
          <div>Following {user.following}</div>
        </div>
      </div>
    )}
  </UserContext.Consumer>
);

// ... all those other components go here ...
// ... (the ones that no longer need to know or care about `user`)

// At the bottom, inside App, we pass the context down
// through the tree using the Provider
class App extends React.Component {
  state = {
    user: {
      avatar: 'https://lh3.googleusercontent.com/-4rElFw0sdsY/AAAAAAAAAAI/AAAAAAAAAA8/TieQ-Ya9NLQ/s640-il/photo.jpg',
      name: 'Jack Frost'
      followers: 1234,
      following: 123
    }
  };

  render() {
    return (
      <div className="app">
        <UserContext.Provider value={this.state.user}>
          <Nav />
          <Body />
        </UserContext.Provider>
      </div>
    );
  }
}

Let’s review this work process:

Provider and Consumer are an inseperable buddies

The Provider and Consumer are bound together. Inseperable. And they only know how to talk to each other. Therefore, if you created two separate contexts, say “Context1” and “Context2”, then Context1’s Provider and Consumer would not be able to communicate with Context2’s Provider and Consumer.

Context holds no state

The Context acts merely as a conduit for you data and does not have its own state. Data value is passed to the Provider, and in turn, gets passed down to any Consumers that know how to look for it.

When you create the context, you can pass in a “default value” like this:

const Ctx = React.createContext(defaultValue);

This default value is what the Consumer will receive when it is placed in a tree with no Provider above it. If you don’t pass one, the value will just be undefined. Note, though, that this is a default value, not an initial value. Due to having no state, a context doesn’t retain anything; it is only in charge of distributing the data you pass in.

Consumer Uses the Render Props Pattern

Redux’s connect function is a higher-order component (or HoC for short). It wraps another component and passes props into it.

The context Consumer, by contrast, expects the child component to be a function. It then calls that function at render time, passing in the value that it got from the Provider somewhere above it (or the context’s default value, or undefined if you didn’t pass a default).

Provider Accepts One Value

Just a single value, as the value prop. But remember that the value can be anything. In practice, if you want to pass multiple values down, you’d create an object with all the values and pass that object down.

That’s pretty much the nuts and bolts of the Context API.

Context API is Flexible

Since creating a context gives us two components to work with (Provider and Consumer), we’re free to use them however we want. Here are a couple ideas.

5.2. Turn the Consumer into a Higher-Order Component

Not fond of the idea of adding the UserContext.Consumer around every place that needs it? Well, it’s your code! You can do what you want. You’re an adult.

If you’d rather receive the value as a prop, you could write a little wrapper around the Consumer like this:

function withUser(Component) {
  return function ConnectedComponent(props) {
    return (
      <UserContext.Consumer>
        {user => <Component {...props} user={user}/>}
      </UserContext.Consumer>
    );
  }
}

And then you could rewrite, say, UserAvatar to use this new withUser function:

const UserAvatar = withUser(({ size, user }) => (
  <img
    className={`user-avatar ${size || '}`}
    alt="user avatar"
    src={user.avatar}
  />
));

And BOOM, context can work just like Redux’s connect. Minus the automatic purity.

Hold State in the Provider The context’s Provider is just a conduit, remember. It doesn’t retain any data. But that doesn’t stop you from making your own wrapper to hold the data.

In the example above, I left App holding the data, so that the only new thing you’d need to understand was the Provider + Consumer components. But maybe you want to make your own “store”, of sorts. You could create a component to hold the state and pass them through context:

class UserStore extends React.Component {
  state = {
    user: {
      avatar:
        'https://lh3.googleusercontent.com/-4rElFw0sdsY/AAAAAAAAAAI/AAAAAAAAAA8/TieQ-Ya9NLQ/s640-il/photo.jpg',
      name: 'Jack Frost'
      followers: 1234,
      following: 123
    }
  };

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

// ... skip the middle stuff ...

const App = () => (
  <div className="app">
    <Nav />
    <Body />
  </div>
);

ReactDOM.render(
  <UserStore>
    <App />
  </UserStore>,
  document
                                          
0