React Context is not for state management
source link: https://www.tuicool.com/articles/hit/YrMR7zA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
React Context is a great new API that makes all sorts of things possible – from react-router to the new Suspense API. But as you may have heard, it also makes some existing tools obsolete. For example, Redux applications have always relied on a <Provider>
component. But now, the Context API comes with a built-in
<Provider>
component!
But wait a moment. Redux’s <Provider>
component has used context since Redux was first created back in 2015. Just look at the connect()
component from Redux’s
initial commit
:
const contextTypes = { observeStores: PropTypes.func.isRequired, bindActions: PropTypes.func.isRequired }; class extends Component { static displayName = `ReduxConnect(${wrappedDisplayName})`; static contextTypes = contextTypes; constructor(props, context) { super(props, context); this.handleChange = this.handleChange.bind(this); this.actions = this.context.bindActions(pickActions); this.unobserve = this.context.observeStores( pickStores, this.handleChange ); } // ... }
Redux uses an older version of the React Context API, but it’s still using React Context. But this raises the question: if Redux has been using context all along, how can context have killed Redux?
The rumors of Redux’s demise have been greatly exaggerated
Now that React has the Context API, Redux is no longer necessary.
– People on the internet
Ok, so let me unpack this.
Firstly, it’s true that Redux isn’t necessary to build a React app. In fact, it’s always been possible to build a decent-sized React app with nothing but component state. As proof, Frontend Armory doesn’t use Redux. And as you might have heard, I’m all for building apps with Raw React .
So let’s say that you’ve started a project without Redux. As it grows, so will the number of elements through which you’ll need to pass your state to get it where it needs to go. Some people call this prop drilling . This can detract from readability, but worse, passing props like this is boring . Luckily, Context makes React fun again!
A brief example
Context API Reference
For more details on the Context API, see React’s official documentation .
Let’s take a look at an example of Context in action. To do so, consider a <Link>
component that captures clicks and passes them to a router, allowing the user to navigate without reloading the page. Let’s say that the <Link>
also adds a special style when its href
matches the window’s current URL.
In order to do all this, the component will need access to an application-wide router
object that holds the current URL, and a method to update it. If you were to manually pass the router
object to the <Link>
element, it might look something like this.
<Link activeStyle={{color: 'red'}} href="/courses/async-javascript/" router={router}> Mastering Async JavaScript </Link>
Of course, passing down a router
prop to every link in your page would be downright tedious. And that’s where Context comes in!
To make your links work with context, you’ll need to do three things:
-
Create a
RouterContext
object usingReact.createContext()
. -
At the top of your application, pass the latest
router
object into a<RouterContext.Provider>
element. -
Within your
<Link>
component, read the currentrouter
object via a<RouterContext.Consumer>
element.
Here’s a demo of what this looks like in practice. I’ve split the app over four files:
-
App.js
creates the latestrouter
object, passes it into a<RouterContext.Provider>
, and renders the application’s content. -
Link.js
defines the<Link>
component. -
contexts.js
creates and exports your context. -
main.js
bootstraps the application.
If you’d to play around with this in create-react-app, just replace the original App
component with this one, and then add Link.js
and contexts.js
.
Link.js
contexts.js
main.js
index.html
New
import { RouterContext } from './contexts.js' import { Link } from './Link.js' export class App extends React.Component { constructor(props) { super(props) // Store the `router` object in component state this.state = { pathname: window.location.pathname, navigate: this.navigate, } // Handle the user clicking the `back` and `forward` buttons window.onpopstate = () => { this.setState({ pathname: window.location.pathname }) } } render() { return ( <RouterContext.Provider value={this.state}> <Link href="/" activeStyle={{color: 'red'}}> Home </Link> <br /> <Link href="/browse" activeStyle={{color: 'red'}}> Browse </Link> </RouterContext.Provider> ) } // The router's `navigate` method updates `router` object, and uses // the browser's `pushState` method to change the window's URL. navigate = (pathname) => { this.setState({ pathname }) // Update the URL within the browser's history window.history.pushState(null, null, pathname) } }
Simple, huh? And since there’s a good chance that you’re familiar with <Link>
components already, let’s do a little quiz.
A Context Quiz
The editor below contains the same example as above, but I’ve added three elements that log a message to the console when they’re rendered.
Your task is to decide which of these console.log()
statements will be executed when you click each of the two links.
Once you’ve decided on your answer, click the links to check!
Link.js
contexts.js
main.js
index.html
New
import { RouterContext } from './contexts.js' import { Link } from './Link.js' // This component logs a message to the console each time // it is rendered or re-rendered. function Log(props) { console.log(`rendering <Log name="${props.name}">`) return null } export class App extends React.Component { constructor(props) { super(props) // Store the `router` object in component state this.state = { pathname: window.location.pathname, navigate: this.navigate, } // Handle the user clicking the `back` and `forward` buttons window.onpopstate = () => { this.setState({ pathname: window.location.pathname }) } } render() { return ( <RouterContext.Provider value={this.state}> <Log name="inside provider" /> <Link href="/" activeStyle={{color: 'red'}}> <Log name="inside home link" /> Home </Link> <br /> <Link href="/browse" activeStyle={{color: 'red'}}> <Log name="inside browse link" /> Browse </Link> </RouterContext.Provider> ) } // The router's `navigate` method updates `router` object, and uses // the browser's `pushState` method to change the window's URL. navigate = (pathname) => { this.setState({ pathname }) // Update the URL within the browser's history window.history.pushState(null, null, pathname) } }
As you can see, the answer is that:
The entire app is re-rendered on every click!
How did you go in the quiz? Were you surprised by the answer? If so, tweet me to let me know! And then, let’s discuss the implications.
Renders aren’t free.
When you update the context by rendering a new value
on a <Provider>
element, all of the provider’s children will be re-rendered too. This means that if your <Provider>
is at the top of your app, then changing the provider’s value
will cause the entire app to re-render! Even in smaller apps, re-rendering everything
can cause a perceivable delay.
Now it should be said that causing a delay after some action isn’t a problem in and of itself. Sometimes the user will expect
actions to take some time. For example, users usually won’t expect immediate feedback when they click a <Link>
element. This makes navigation state a perfect candidate to be passed down via Context.
But imagine for a moment that a form’s state is stored in your root component and passed down via Context. Now each keystroke within the form will cause your entire app to re-render! I won’t claim to be able to read your users’ minds, but I’m going to hazard a guess that being made to wait half a second for a keystroke to appear is going to piss some people off.
But wait a minute.
Doesn’t Redux also put a <Provider>
at the top of the app?
Values vs. Stores
Redux and Apollo both
require that you place a <Provider>
element at the top of your application. But there’s something special about their providers.
For example, here’s how you’d use Redux’s <Provider>
element – taken from Redux’s
documentation
.
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' const store = createStore(todoApp) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
There’s one major difference between this top-level <Provider>
and the one from the earlier quiz. Do you know what this difference is?
Have a think about it, and then check your answer below.
This provider is only ever rendered once! The value
prop never changes; it is always just store
.
With Redux and Apollo, Context is not used to propagate the latest state. Instead, it is used to propagate the entire store instance .
But with this being the case, how do changes in the store get propagated to components that use the state? Easy! The store has a listen()
method. Redux’s connect()
higher-order-component subscribes to store updates via store.listen()
, and saves the latest selected data to component state.
In a Redux-based app, re-renders aren’t caused by updates to context.Instead, they occur when connect()
saves new information to component state. This helps Redux minimize the performance impact that a global context-based store would have.
Where Context Shines
If your components make use of frequently changing data like forms or HTTP responses, it’s best to keep that data in component state . This makes it possible to re-render individual components in response to user actions, as opposed to re-rendering everything.
But while Context struggles with some things, there are other situations where it really shines. In particular, Context is perfect for providing infrequently changing objects to children. To give you some concrete examples, here are some objects that the Frontend Armory app passes down via Context:
- Navigation/routing details
- Authentication state
- A boolean indicating whether the app is being statically rendered, or is in a real browser
I don’t have time to go into the details of these use cases today, but I’ll cover them in a future post!
Thanks so much for reading. If you haven’t already, become a free member to make sure you don’t miss out on future posts – and to get access to five printable cheatsheets . Or if you prefer, follow me on twitter instead! And as always, I love hearing your questions / comments. Hit me up on twitter, or send me an e-mail anytime at [email protected] . I can’t wait to hear from you!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK