64

React Context is not for state management

 5 years ago
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.
neoserver,ios ssh client

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:

  1. Create a RouterContext object using React.createContext() .
  2. At the top of your application, pass the latest router object into a <RouterContext.Provider> element.
  3. Within your <Link> component, read the current router 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 latest router 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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK