• Redux vs. The React Context API
  • Written by Dave Ceddia
  • Translation from: The Gold Project
  • This article is permalink: github.com/xitu/gold-m…
  • Translator: Xuyuey
  • Proofreader: Minghao, Baddyo

React has introduced a new Context API in version 16.3 — new because the old Context API was an experimental feature behind the scenes that most people either don’t know about or, according to the official documentation, try to avoid.

However, now the Context API has become a first class citizen in React, open to everyone (unlike before, which is now officially promoted).

When React version 16.3 was released, articles announced that the new Context API would ban Redux flooded the Web. But if you ask Redux, I think it would say, “The reports of my death have been greatly exaggerated.”

In this article, I want to show you how the new Context API works, how it’s similar to Redux, when you can use the Context API instead of Redux, And why the Context API doesn’t replace Redux in all cases.

If you just want an overview of the Context, fineSkip to this section.

A simple React example

This assumes you already know the React basics (props and state), but if you don’t, you can take one of my free 5-day courses to learn the React basics.

Let’s look at an example where Redux is accessible to most people. We’ll start with a simple React version, then see what it looks like in Redux, and finally Context.

In the app, user information is displayed in two places: the upper right corner of the navigation bar and the sidebar next to the main content.

(You might notice that it looks a lot like Twitter. This is no accident! One of the best ways to hone your React skills is through copying — building copies of existing apps).

The component structure is as follows:

With pure React (just regular props), we need to store user information high enough in the component tree that we can pass it down to every component that needs it. In our example, user information must be stored in the App.

Next, in order to pass the user information down to the component that needs it, the App needs to first pass it to Nav and Body. It is then passed down again to the UserAvatar (hooray! Finally) and Sidebar. Finally, Sidebar passes it to UserStats.

Let’s take a look at how the code works (I’ve put everything in one file for easy reading, but in reality it might be divided into several files with some standard structure).

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 UserStats = ({ user }) => (
  <div className="user-stats">
    <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">
    <UserStats 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://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
      name: "Dave",
      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"));
Copy the code

Check out the online example in CodeSandbox.

In this case, the App initializes state with the “user” object in it — but in a real application you might need to fetch that data from the server and save it in state for rendering.

This kind of prop drilling is not a bad idea. It works pretty well. Not all situations discourage Prop drilling; It’s a perfectly effective model and is at the heart of what supports React. But if the component is too deep, it can get a little annoying while you’re writing it. Especially when you pass down not just one property, but a bunch of them, it gets even more annoying.

However, this “prop drilling” strategy has a bigger drawback: it allows components that should be separate to be coupled together. In the example above, the Nav component needs to receive a “user” attribute and pass it to the UserAvatar, even though there is no other place in the Nav where the user attribute is needed.

Tightly coupled components (like those that pass properties to their children) are harder to reuse, because whenever you use them in a new place, you have to associate them with the new parent.

Let’s see how we can improve.

Before using Context or Redux…

If you can find a way to incorporate the structure of your application and take advantage of the children property, you can make the code structure clearer without having to use deep prop drilling or Context or Redux.

For components that need to use generic placeholders, such as Nav, Sidebar, and Body in this case, the children property is a good solution. Also know that you can pass JSX elements to any attribute, not just “children” — so keep this in mind if you want to use more than one “slot” to insert components.

In this example Nav, Sidebar, and Body receive children and render them as they look. In this way, the consumer of the component doesn’t have to worry about specific data being passed to the component — he just needs to use the data defined within the component and simply render the component according to its original requirements. This example also shows how to pass children using arbitrary attributes.

(Thanks to Dan Abramov for that advice!)

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 UserStats = ({ user }) => ( <div className="user-stats"> <div> <UserAvatar user={user} /> {user.name} </div> <div className="stats"> <div>{user.followers} Followers</div> <div>Following {user.following}</div> </div> </div> ); // Receive and render children 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 can be written in this way, // They can be any property const Body = ({sidebar, content }) => ( <div className="body"> <Sidebar>{sidebar}</Sidebar> {content} </div> ); class App extends React.Component { state = { user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }; render() { const { user } = this.state; return ( <div className="app"> <Nav> <UserAvatar user={user} size="small" /> </Nav> <Body sidebar={<UserStats user={user} />} content={<Content />} /> </div> ); } } ReactDOM.render(<App />, document.querySelector("#root"));Copy the code

Check out the online example in CodeSandbox.

If your application is too complex (more complex than this example!) , it may be hard to figure out how to tweak the children model. Let’s see how to replace Prop drilling with Redux.

Use the Redux example

I’m going to go through the Redux example quickly here, so we can take a little more time to dive into how Context works, so if you’re not familiar with Redux, check out my introduction to Redux (or watch the video) first.

We’re still using the React app above, but here we’re refactoring it to the Redux version. The user information is moved into the Redux store, which means we can use the React-Redux connect function to inject the User property directly into the component that needs it.

This is a huge win in terms of decoupling. If you look at Nav, Sidebar, and Body, you will see that they no longer receive and pass down user properties. I don’t need to play props anymore. There is certainly no more coupling than is necessary.

The Reducer here doesn’t do a lot of work; Very simple. I have more articles elsewhere on how Redux Reducer works and how to write immutable code in it that you can check out.

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 the initial state empty
const initialState = {};
function reducer(state = initialState, action) {
  switch (action.type) {
    // Respond to the SET_USER behavior and update
    // The corresponding state
    case "SET_USER":
      return {
        ...state,
        user: action.user
      };
    default:
      returnstate; }}// Create the Store using reducer
const store = createStore(reducer);

// Triggers the action of setting user
// (because user is null when initialized)
store.dispatch({
  type: "SET_USER".user: {
    avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b".name: "Dave".followers: 1234.following: 123}});// The mapStateToProps function extracts the user value from the state object
// and pass it as the 'user' attribute
const mapStateToProps = state= > ({
  user: state.user
});

// connect() UserAvatar so that it can receive 'user' attributes directly,
// Without having to fetch it from the upper component

// It can also be divided into the following two 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() UserStats so that it can receive 'user' attributes directly, Const UserStats = connect(mapStateToProps)(({user}) => (<div) className="user-stats"> <div> <UserAvatar /> {user.name} </div> <div className="stats"> <div>{user.followers} Followers</div> <div>Following {user.following}</div> </div> </div> )); // Nav no longer needs to know the 'user' attribute const Nav = () => (<div className=" Nav "> <UserAvatar size="small" /> </div>); const Content = () => ( <div className="content">main content here</div> ); Const Sidebar = () => (<div className=" Sidebar "> <UserStats /> </div>); Const body = () => (<div className="body"> <Sidebar /> <Content /> </div>); Const App = () => (<div className=" App "> <Nav /> <Body /> </div>); // Wrap the entire App with Provider, Reactdom.render (<Provider store={store}> <App /> </Provider>, document.querySelector("#root") );Copy the code

Check out the online example in CodeSandbox.

By now you may be wondering how Redux can achieve such magic. “Wondering” is a good thing. React doesn’t support passing attributes across multiple levels, so why does Redux?

The answer is that Redux uses the React context feature. Not the Context API we’re talking about now (not yet) — but the old one. The one that the React document says don’t use unless you’re writing a library or you know what you’re doing.

Context is like an electronic bus running behind each component: to receive the power (data) it sends, you just plug in the plug. And that’s what the connect function of (React-) Redux does.

This feature of Redux, however, is just the tip of the iceberg. The ability to pass data anywhere is only the most obvious feature of Redux. Here are some other benefits you can get out of the box:

connectMake your components pure

Connect makes connected components “pure,” meaning they only need to be rerendered when their properties change — that is, when their Redux state slice changes. This will prevent unnecessary repetitions and make your application run faster. DIY method: create a class that inherits PureComponent, or implement shouldComponentUpdate yourself.

Easy debugging with Redux

Although writing to action and Reducer is a bit more complex, we can balance this with the powerful debugging capabilities it gives us.

With the Redux DevTools extension, every action performed by the application is automatically recorded. You can turn it on at any time to see what action was triggered, what the payload was, and the state before and after the action took place.

Redux DevTools offers another great feature — Time Travel Debugging. That is, you can click on any past action and jump to that point in time. It basically replays every action, including the current one. But not actions that haven’t been triggered yet. The idea is that every action immutably updates state, so you can take a list of state updates and replay them, jumping to where you want to go, without any side effects.

And there are tools like LogRocket that give every one of your users an alway Redux DevTools in a production environment. Have bug reports? it doesn’t matter Look in LogRocket for that user’s session, and you can see everything they did and exactly what action was triggered. All of this can be done using Redux’s flow of operations.

Custom Redux using middleware

Redux supports the concept of middleware, which stands for “a function that runs every time an action is scheduled.” Writing your own middleware isn’t as difficult as it seems, and it can achieve some powerful features.

For example…

  • I want to name it in each nameFETCH_Submit THE API request in the opening operation? You can use middleware.
  • Want to log events in a centralized place for your analytics software? Middleware is a good place to be.
  • Want to prevent certain behaviors from triggering at certain times? You can do it in middleware, and it’s transparent to the rest of the application.
  • Want to intercept operations with JWT tokens and automatically save them to localStorage? Yes, you can also use middleware.

Here’s a good article with some examples of how to write Redux middleware.

How do I use the React Context API

But maybe you don’t need all the bells and whistles of the Redux. Maybe you don’t care about simple debugging, customization, or automated performance improvements — all you want to do is pass data easily. Maybe your app is small, or maybe you just need to get it working now and worry about all the bells and whistles later.

React’s new Context API might suit your needs. Let’s see how it works.

If you’d rather watch the video (3:43) than read the article, I’ve posted a short Context API course on Egghead:

There are three important parts to the Context API:

  • React.createContextFunction: Creates the context
  • Provider(bycreateContextReturn) : Build the “electronic bus” in the component tree
  • Consumer(by the samecreateContextReturn) : Access the “electronic bus” to obtain data

The Provider here is very similar to the Provider in React-Redux. It takes a value attribute, which can be anything you want (even a Redux store… But that’s silly). It is most likely an object that contains your data and the actions you want to perform on it.

Consumer here works a bit like the React-Redux connect function, receiving data for use by components.

Here are the highlights:

// At the beginning, we create a new context
// It is an object with two properties {Provider, Consumer}
// Note that the name is UpperCase, not camelCase
// This is important because we will use it as a component later
The name of the component must begin with an uppercase letter
const UserContext = React.createContext();

// Below are the components that need to get data from the context
// This can be done by using the Consumer property of UserContext
// Consumer uses the "render props" mode
const UserAvatar = ({ size }) = >( <UserContext.Consumer> {user => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )} </UserContext.Consumer> ); Const UserStats = () => (< userContext.consumer > {user => (<div className="user-stats"> <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 the other components... / /... (Those that don't use 'user')... Class App extends React.Com {state = {user: {avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }; render() { return ( <div className="app"> <UserContext.Provider value={this.state.user}> <Nav /> <Body /> </UserContext.Provider> </div> ); }}Copy the code

Here is the complete example in CodeSandbox.

Let’s see how it works.

Remember that there are three parts: the context itself (created by React.createcontext), and the two components that talk to it (Provider and Consumer).

Provider and Consumer are gay friends

Providers and consumers are tied together. Like peas and carrots. And they only know how to talk to each other. If you create two separate contexts, such as “Context1” and “Context2”, then it is impossible for the Provider and Consumer of Context1 to communicate with the Provider and Consumer of Context2.

State is not saved in the context

Note that the context does not have its own state. It’s just a conduit for data. You must pass the value to the Provider, and then the exact value will be passed to any Consumer who knows how to get it (the Consumer and the Provider are bound to the same context).

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

const Ctx = React.createContext(yourDefaultValue);
Copy the code

When a Consumer is placed on a tree without a Provider package, it will receive this default value. If you don’t pass in the default value, the value will be undefined. Note that this is the default value, not the initial value. The context retains nothing; It just distributes the data that you pass in.

The Consumer uses the Render Props mode

Redux’s Connect function is a high-order component (or HoC for short). It wraps another component and passes the props to it.

The context Consumer, on the other hand, expects the child component to be a function. It then calls this function at render time, passing the value it got from the Provider that wrapped it (or the context default, or undefined if you didn’t pass the default) to the child component.

The Provider receives a single value

It receives the value attribute, which is a single value. But remember this value can be anything. In practice, if you want to pass multiple values down, you must create an object containing those values and pass that object down.

This is pretty much the heart of the Context API.

Flexible Context API

Because the creation context gives us two components (Provider and Consumer) to use, we are free to use them. Here are a few ideas.

Turn the Consumer into a high-level component

Don’t like the idea of adding userContext. Consumer usage to every place you need it? Well, here’s your code! You can do whatever you want. You’re a grown man.

If you prefer to accept a value as a property, you can write a wrapper for the Consumer, like this:

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

Then you can rewrite your code, such as the UserAvatar component using the new withUser function:

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

BOOM, context can work just like Redux’s Connect. Keep your components pure.

Here is an example of CodeSandbox with this high-level component.

Use the Provider to save the state

Remember, the context Provider is just a pipeline. It does not retain any data. But that doesn’t stop you from making your own wrappers to hold the data.

In the example above, I used the App to save the data, so the only new thing you need to know here is the Provider + Consumer component. But maybe you want to write your own “store,” etc. You can create a component to hold the data and pass it through the context.

class UserStore extends React.Component {
  state = {
    user: {
      avatar:
        "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b".name: "Dave".followers: 1234.following: 123}}; render() {return (
      <UserContext.Provider value={this.state.user}>
        {this.props.children}
      </UserContext.Provider>); }} / /... Skip the middle... const App = () => (<div className="app">
    <Nav />
    <Body />
  </div>
);

ReactDOM.render(
  <UserStore>
    <App />
  </UserStore>,
  document.querySelector("#root")
);
Copy the code

Now, your user data is nicely contained in its own component, which is focused solely on user data. Great. Apps can become stateless components again. I think it looks neater.

Here is an example of CodeSandbox with this UserStore.

Operations are passed by context

Remember that objects passed through a Provider can contain anything you want. That means it can contain functions. You could even call it an action.

Here’s a new example: a simple room with a switch that switches the color of the background — sorry, I mean the light.

The State is stored in the store, and the store also has a function to switch lights. Both State and function are passed by context.

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

// Simple empty context
const RoomContext = React.createContext();

// A component
// The only job is to manage Room state
class RoomStore extends React.Component {
  state = {
    isLit: false
  };

  toggleLight = (a)= > {
    this.setState(state= > ({ isLit: !state.isLit }));
  };

  render() {
    // Pass state and onToggleLight operations
    return( <RoomContext.Provider value={{ isLit: this.state.isLit, onToggleLight: this.toggleLight }} > {this.props.children} </RoomContext.Provider> ); Const Room = () => (< roomContext.consumer > {({isLit, onToggleLight }) => ( <div className={`room ${isLit ? "lit" : "dark"}`}> The room is {isLit ? "lit" : "dark"}. <br /> <button onClick={onToggleLight}>Flip</button> </div> )} </RoomContext.Consumer> ); const App = () => ( <div className="app"> <Room /> </div> ); Reactdom.render (<RoomStore> <App /> </RoomStore>, document.querySelector("#root") );Copy the code

Here is the complete example in CodeSandbox.

Should I use Context or Redux?

Now that you’ve seen both — which one should you use? Well, one of the things that will make your app better and more fun to write is making decisions. I know you probably just want “answers,” but I’m sorry to say, “It depends.”

It depends on how big your app is or will become. How many people will be involved — just you or a larger team? Your or your team’s experience with the functional concepts that Redux relies on, such as invariance and pure functions.

One of the great pernicious fallacies in the JavaScript ecosystem is the concept of competition. The idea is that every choice is a zero-sum game; If you use library A, you cannot use its competitor library B. The idea is that when a new library comes along that is somehow better, it must replace the existing library. It’s one of those “or… or… You either have to choose the best library at the moment, or use the previous library with people from the past.

A better approach is to have something like a toolbox where you can put all your options. It’s like choosing between a screwdriver and a hammer drill. For 80% of the work, a hammer drill is faster than a screwdriver to unscrew. But for the other 20 percent, a screwdriver is actually a better choice — perhaps because the space is small, or the object is delicate. When I have a hammer drill, I don’t immediately throw away my screwdriver, or even my non-hammer drill. The impact drill didn’t replace them, it just gave me another option. Another way to solve the problem.

React will “replace” Angular or jQuery, but Context will not “replace” Redux like this. Heck, I still use jQuery when I need to get something done quickly. I still sometimes use server-rendered EJS templates instead of the entire React application. Sometimes React is more demanding than the task at hand. Sometimes Redux has features you don’t need.

Now, when Redux exceeds your needs, you can use Context.

translation

  • Russian (courtesy of Maxim Vashchenko)
  • Japanese (provided by Kiichi)
  • Portuguese (Courtesy of Wenderson Pires)

If you find any errors in the translation or other areas that need improvement, you are welcome to revise and PR the translation in the Gold Translation program, and you can also get corresponding bonus points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


Diggings translation project is a community for translating quality Internet technical articles from diggings English sharing articles. The content covers the fields of Android, iOS, front end, back end, blockchain, products, design, artificial intelligence and so on. For more high-quality translations, please keep paying attention to The Translation Project, official weibo and zhihu column.