📜 ⬆️ ⬇️

Redux vs React Context API



A new Context API has been added to React 16.3. New in the sense that the old Context API was off-screen, most people either did not know about its existence, or did not use it, because the documentation advised to avoid using it.

However, now the Context API is a full-fledged part of React, open for use (not as it used to be, officially).

Immediately after the release of React 16.3, there were articles that proclaimed the death of Redux due to the new Context API. If you asked Redux about this, I think he would answer - “the reports of my death are greatly exaggerated .”
')
In this post I want to talk about how the new Context API works, how it looks like Redux, when you can use Context instead of Redux, and why Context does not replace Redux in each case.

If you just need an overview of the Context API, you can follow the link .

Sample React Application


I'm going to assume that you have an understanding of how to work with a state in React (props & state), but if not, I have a free 5-day course that will help you learn about it .

Let's look at an example that will lead us to the concept used in Redux. We'll start with a simple version of React, and then see how it looks in Redux and, finally, with Context.



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

(You may notice that there is a great similarity with Twitter. Not by chance! One of the best ways to hone your React skills is to copy (create replicas of existing sites / applications) .

The structure of the component looks like this:



Using pure React (props only) we need to store user information high enough in the tree so that it can be passed on to the components that need it. In this case, user information must be in the App.

Then, in order to pass information about the user to the components that need it, the application must pass it to Nav and Body. They, in turn, will pass it to UserAvatar (hurray!) And Sidebar. Finally, Sidebar should pass it on to UserStats.

Let's take a look at how this works in the code (I put everything in one file to make it easier to read, but in fact it will probably be divided into separate files, following some kind of 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")); 


Sample CodeSandbox Code

Here the App initializes the state containing the “user” object. In a real application, you will most likely retrieve this data from the server and save it in state for rendering.

As for props forwarding (“prop drilling”), this is not a problem . It works fine. “Forwarding props” is the perfect example of React work. But forwarding to the depth of the state tree can be a little annoying when writing. And annoy more and more if you have to transfer a lot of props (and not just one).

Nevertheless, there is a big minus in this strategy: it creates a connection between the components that should not be connected. In the example above, Nav should accept the “user” prop and pass it to UserAvatar, even if Nav does not need it.

The closely related components (for example, those that transfer props to their children) are more difficult to reuse, since you must attach them to new parents whenever you use them in a new place.

Let's see how we can improve this.

Before using Context or Redux ...


If you can find a way to unify the structure of your application and take advantage of the transfer of props to descendants, this can make your code cleaner without having to resort to deep probros props, Context , or Redux .

In this example, children props is a great solution for components that should be universal, such as Nav, Sidebar and Body. Also know that you can pass JSX to any prop, not just children - so if you need more than one “slot” for connecting components, keep this in mind.

Here is an example of a React application in which Nav, Body and Sidebar take child elements and display them as is. Thus, the person who uses the component does not need to worry about the transfer of certain data that is required by the component. He can simply display what he needs in place, using the data he already has in scope. This example also shows how to use any prop to transfer children.

(Thanks to Dan Abramov for this offer !)

 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> ); //  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   sidebar  content,    , //    . 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")); 


Sample CodeSandbox Code

If your application is too complex (more complicated than this example!), It may be difficult to understand how to adapt the pattern for children. We'll see how you can replace props probros with Redux.

Redux example


I will quickly review an example on Redux so that we can better understand how Context works, so if you do not have a clear understanding of how Redux works, first read my introduction to Redux (or watch the video ).

Here is our React application, converted to use Redux. User information has been moved to the Redux store, which means that we can use the react-redux connect function to directly transfer user prop to the components that need them.

This is a big victory in terms of getting rid of connectedness. Take a look at Nav, Body and Sidebar, and you will see that they no longer accept or transfer user prop. No longer play hot potatoes with props. No more useless links.

Reducer does little here; it's pretty simple. I have something else about how Redux reducers work and how to write the immutable code that is used in them.

 import React from "react"; import ReactDOM from "react-dom"; //    createStore, connect, and Provider: import { createStore } from "redux"; import { connect, Provider } from "react-redux"; //  reducer       . const initialState = {}; function reducer(state = initialState, action) { switch (action.type) { //    action SET_USER  state. case "SET_USER": return { ...state, user: action.user }; default: return state; } } //  store  reducer'   . const store = createStore(reducer); // Dispatch' action     user. store.dispatch({ type: "SET_USER", user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }); //   mapStateToProps,      state (user) //     `user` prop. const mapStateToProps = state => ({ user: state.user }); //  UserAvatar    connect(),    //`user` ,      . //     2 : // 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} /> )); //   UserStats    connect(),    // `user` . 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      `user`. const Nav = () => ( <div className="nav"> <UserAvatar size="small" /> </div> ); const Content = () => ( <div className="content">main content here</div> ); //   Sidebar. const Sidebar = () => ( <div className="sidebar"> <UserStats /> </div> ); //   Body. const Body = () => ( <div className="body"> <Sidebar /> <Content /> </div> ); //  App    ,     . const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); //     Provider, //   connect()    store. ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.querySelector("#root") ); 


Sample CodeSandbox Code

Now you probably wonder how Redux achieves this magic. Amazing. How does React not support the transfer of props to several levels, and can Redux do this?

The answer is that Redux uses the context function React (context feature). Not a modern Context API (not yet), but an old one. One that in the React documentation is told not to use if you are not writing your library or do not know what you are doing.

The context is similar to the computer bus following each component: to get the power (data) passing through it, you only need to connect. And react-redux connect does exactly that.

However, this Redux feature is only the tip of the iceberg. Transferring data to just the right place is the most obvious of the features of Redux. Here are some other benefits you get out of the box:

connect is a pure function

connect automatically makes the connected components "clean", that is, they will be re-rendered only when their props change - that is, when their Redux state slice changes. This prevents unnecessary re-rendering and speeds up the application.

Easy debugging with Redux

Writing out actions and reducers is balanced by the amazing debugging ease Redux provides you.

With the Redux DevTools extension, you get an automatic log of all actions performed by your application. At any time, you can open it and see what actions were launched, what payload they had, and state before and after the action.



Another great feature that Redux DevTools provides is debugging with the help of “time travel” , so you can click on any previous action, and go to this point in time, up to the current one. The reason this works is that each action updates the store in the same way , so you can take a list of the recorded status updates and play them without any side effects, and finish in the right place.

There are also tools like LogRocket , which basically give you permanent Redux DevTools in production for each of your users. Got a bug report? No problem. View this user session in LogRocket, and you can see a repetition of what he did and which actions were launched. It all works using the Redux action stream.

Extending Redux with Middleware

Redux supports the concept of middleware (a fancy word for "a function that runs every time an action is sent"). Writing your own middleware is not as difficult as it may seem, and allows you to use some powerful tools.

For example…


Here is a good article with examples of how to write redux middleware.

How to use React Context API


But maybe you do not need all these oddities of Redux. Perhaps you don’t need simple debugging, tuning, or automatic performance improvements — all you want to do is transfer data easily. Maybe your application is small, or you just need to quickly do something and deal with the subtleties later.

The new Context API will probably suit you. Let's see how it works.

I posted a quick lesson on Context API on Egghead, if you like looking more than reading (3:43).

Here are 3 important components of the Context API:


Provider is very similar to Provider in React-Redux. It takes on a value that can be anything you want (it can even be a store Redux ... but that would be stupid). Most likely, this is an object containing your data and any actions that you want to perform with the data.

Consumer works a bit like the connect function in React-Redux, connecting to the data and making it available to the component that uses it.

Here are the highlights:

 //     context //    2 : { Provider, Consumer } // ,   ,  UpperCase,  camelCase //  ,          , //        . const UserContext = React.createContext(); // ,     context, //   Consumer. // Consumer   "render props". const UserAvatar = ({ size }) => ( <UserContext.Consumer> {user => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )} </UserContext.Consumer> ); // ,      "user prop", //   Consumer    context. 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> ); // ...    ... // ... (      `user`). //  App   context ,  Provider. class App extends React.Component { 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> ); } } 


Sample CodeSandbox Code

Let's take a look at how this works.

Remember, we have 3 parts: the context itself (created using React.createContext) and two components that interact with it (Provider and Consumer).

Provider and Consumer work together

Provider and Consumer are interconnected and inseparable. They only know how to interact with each other. If you create two separate contexts, say “Context1” and “Context2”, then Provider and Consumer Context1 will not be able to communicate with Provider and Consumer Context2.

Context does not contain state

Note that context does not have its own state . This is just a feed for your data. You must pass the value to the Provider, and this value is passed to any Consumer that knows how to find it (the Provider is tied to the same context as the Consumer).

When you create a context, you can pass a “default value” as follows:

 const Ctx = React.createContext(yourDefaultValue); 


The default value is what the Consumer will receive when placed in a tree without Provider above it. If you do not pass it, the value will be undefined. Note that this is the default value , not the initial value. ontext does not save anything; it simply distributes the data you transmit to it.

Consumer uses Render Props pattern

The connect Redux function is a higher order component (abbreviated as HoC). It wraps another component and passes props into it.

Consumer, by contrast, expects the child component to be a function. It then calls this function during rendering, passing the value it received from Provider somewhere above it (or the default value from the context, or undefined if you don’t pass the default value).

Provider takes one value

Just one value, like prop. But remember that the value can be anything. In practice, if you want to pass several values ​​down, you must create an object with all the values ​​and pass this object down.

Context API flexible


Since creating a context gives us two components to work with (Provider and Consumer), we can use them as we want. Here are a couple of ideas.

Wrap a Consumer in a HOC

Don't like the idea of ​​adding a UserContext.Consumer around every place that needs it? This is your code! You have the right to decide what will be the best choice for you.

If you prefer to get the value as a prop, you can write a small wrapper around the Consumer as follows:

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

After that, you can rewrite, for example, UserAvatar using the withUser function:

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

And voila, context can work the same way as connect Redux. Minus automatic cleanliness.

Here is an example of CodeSandbox with this HOC.

Keep State in Provider

Remember that Provider is just a channel. It does not save any data. But this does not prevent you from making your own data wrapper.

In the example above, the data is stored in the App, so the only thing you need to understand is the components Provider + Consumer. But maybe you want to create your own store. You can create a component to store the state and pass them 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> ); } } // ...    ... const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); ReactDOM.render( <UserStore> <App /> </UserStore>, document.querySelector("#root") ); 

Now the user data is contained in its own component, the only task of which is this data. Cool. The app can again become clean (stateless). I think it looks a bit cleaner.

Here is an example of CodeSandbox with this UserStore.

Throw actions down through context

Remember that the object passed through the Provider may contain everything you want. This means that it may contain functions. You can even call them actions.

Here is a new example: a simple room with a switch to switch the background color - oh, I mean light.



State is stored in the store, which also has the function of switching light. Both state and function are passed through the context.

 import React from "react"; import ReactDOM from "react-dom"; import "./styles.css"; //  context. const RoomContext = React.createContext(); // ,     //   . class RoomStore extends React.Component { state = { isLit: false }; toggleLight = () => { this.setState(state => ({ isLit: !state.isLit })); }; render() { //  state  onToggleLight action return ( <RoomContext.Provider value={{ isLit: this.state.isLit, onToggleLight: this.toggleLight }} > {this.props.children} </RoomContext.Provider> ); } } //    ,    , //       RoomContext. 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> ); //     RoomStore, //           . ReactDOM.render( <RoomStore> <App /> </RoomStore>, document.querySelector("#root") ); 

Here is a full working example in CodeSandbox .

So after all, what to use, Context or Redux?

Now that you've seen both ways - which one should you use? I know that you just want to hear the answer to this question, but I have to answer - "depends on you."

It depends on how big your application is now or how fast it will grow. How many people will work on it - just you or a big team? How experienced are you or your team in working with the functional concepts on which Redux relies (such as immunity and pure functions).

A disastrous mistake that permeates the entire JavaScript ecosystem is the idea of competition . There is an idea that each choice is a zero-sum game: if you use library A, you should not use its competitor library B. What happens when a new library comes out, which is something better than the previous one, it should supplant the existing one. That everything should be either / or that you should either choose the newest and best, or be upstaged along with the developers from the past.

The best approach is to look at this wonderful choice with an example of a set of tools. This is like choosing between using a screwdriver or a powerful screwdriver. For 80% of cases, the screwdriver will do the job easier and faster than a screwdriver. But for the other 20% a screwdriver would be the best choice (little space, or a thin object). When I bought a screwdriver, I did not immediately throw out the screwdriver, he did not replace it, but simply gave me another option. Another way to solve the problem.

Context does not "replace" Redux, nothing more than React "replaced" Angular or jQuery. Hell, I still use jQuery when I need to do something quickly. I still occasionally use server-side EJS templates instead of deploying React. Sometimes React is more than you need to complete a task. The same goes for Redux.

Today, if Redux is more than you need, you can use context.

Learning React can be hard - so many libraries and tools!

My advice? Ignore them all :)

For step-by-step learning, read my book Pure React .

Source: https://habr.com/ru/post/419449/


All Articles