Upgrading to React-Redux v6: Around the New Context API

So React-Redux upgraded to 6.0.0. I've spent some time to migrate our codebase. Here's a bit of what I've learned.

In this writeup I will cover the following topics about React-Redux v6:

  • Using custom context
  • Accessing the store
  • Supporting multiple stores

This writeup does not cover the following topic, although they're also changes to React-Redux's API after v6:

  • Replacing withRef with forwardRef
  • Deprecated createProvider()

Major Changes

The major implementation change of React-Redux v6 is that it migrates from using React's Legacy Context API to React's New Context API. It mainly affects how it access the store internally, and how it allows its user apps to access the store.

This means that if your app is only using React-Redux’s two major APIs <Provider /> and connect, chances are it will just work.

Other changes include deprecating directly passing store as props to connected components, deprecating multiple stores via storeKey, deprecating createProvider, etc.

Here is a short list of libraries that were initially broken by React-Redux v6, and have released (or in beta phase) their newest support:

If you are using React-Router-Redux, this library has been deprecated and is no longer maintained in favor of Connected-React-Router. You may refer to Connected-React-Router’s doc for reference on migration.

Providing Custom Context

Instead of using the default context instance from React-Redux, you may supply your own context object.

<Provider context={MyContext} store={store}>
  <App />
</Provider>

If you supply a custom context, React-Redux will use that context instance instead of its default one.

Note that with React’s new context API, while it is possible to nest <Context.Provider />, the provided value to the nearest ancestor provider will be used. Values provided in earlier ancestors will not be consulted or merged. ~~This means you are not supposed to nest your custom context’s provider beneath React-Redux’s <Provider />. It will break React-Redux’s usage.~~ More explanations about the context API can be found here.

Note: I later learned about this issue where shadowing with nesting context's provider is a legit use case, and in that case a brilliant solution. I guess I should not have said something along the lines of "you are not supposed to..."

After you’ve supplied the custom context to <Provider />, you will also need to supply this context instance to all of your connected components:

export default connect(
  mapState,
  mapDispatch,
  null,
  {
    context: MyContext,
  }
)(MyComponent)

// or
const ConnectedComponent = connect(
  mapState,
  mapDispatch
)(MyComponent)
;<ConnectedComponent context={MyContext} />

Not providing a context to connected components will result in runtime error:

Invariant Violation

Could not find "store" in the context of "Connect(MyComponent)". Either wrap the root component in a , or pass a custom React context provider to and the corresponding React context consumer to Connect(Todo) in connect options.

Here's our async inject reducer in a CodeSandbox: Asynchronously inject reducer using React-Redux v6 and custom context.

Accessing the Store

Grabbing the store from context or from importing other files seems to have never been recommended by the library's maintainers. Nevertheless, it can be quite common anyway.

React-Redux official doc

It's an anti-pattern to interact with the store directly in a React component, whether it's an explicit import of the store or accessing it via context.

In v6, React-Redux no longer uses React’s Legacy Context API. Instead, it uses React’s New Context API. This means the old way of accessing store by defining contextTypes won’t work.

React-Redux exports the default context instance it uses for <Provider /> so that you may access the store by doing this:

import { ReactReduxContext } from 'react-redux'

// in your connected component
render() {
  return (
    <ReactReduxContext.Consumer>
      {({ store }) => <div>{store}</div>}
    </ReactReduxContext.Consumer>
  )
}

I have forked the last CodeSandbox example with a cleaner implementation: Asynchronously inject reducer with React-Redux v6 using default context.

Supporting Multiple Stores

Once again using multiple stores is never recommended neither. The whole Redux v.s. Flux discussion seem to have drawn a clear line:

Can or should I create multiple stores? Can I import my store directly, and use it in components myself?

The original Flux pattern describes having multiple “stores” in an app, each one holding a different area of domain data. This can introduce issues such as needing to have one store “waitFor” another store to update. This is not necessary in Redux because the separation between data domains is already achieved by splitting a single reducer into smaller reducers.

Specifying multiple stores and accessing them with storeKey is deprecated in v6. However, it is possible to implement it by providing (multiple) custom context, and have different stores live in different contexts.

// a naive example

// there is no need to supply a default value when creating the context
// the value will be supplied when React-Redux mounts with your Context.Provider
const ContextA = React.createContext()
const ContextB = React.createContext()

// assuming reducerA and reducerB are proper reducer functions
const storeA = createStore(reducerA)
const storeB = createStore(reducerB)

// rendering
return (
  <Provider store={storeA} context={ContextA}>
    <Provider store={storeB} context={ContextB}>
      <App />
    </Provider>
  </Provider>
)

It is possible to chain connect()

import { compose } from 'redux'
import { connect } from 'react-redux'

compose(
  connect(
    mapStateA,
    null,
    null,
    { context: ContextA }
  ),
  connect(
    mapStateB,
    null,
    null,
    { context: ContextB }
  )
)(MyComponent)

CodeSandbox example: A reading list app with theme using a separate store, implemented by providing (multiple) custom context.

{% codesandbox 92pm9n2kl4 %}

From a development experience perspective, I feel that the new context API provides a clearer isolation for multiple stores. Perhaps it can be less inadvisable at this time?

Links and References

And some issue threads

There are plenty of places to get help