Idiomatic Redux: The History and Implementation of React-Redux

This is a post in the Idiomatic Redux series.


Some history and explanations of how React-Redux got its API, and how it works internally

Intro

React-Redux is conceptually pretty simple. It subscribes to the Redux store, checks to see if the data your component wants has changed, and re-renders your component.

However, there's a lot of internal complexity to make that happen, and most people aren't aware of all the work that React-Redux does internally. I'd like to dig through some of the design decisions and implementation details of how React-Redux works, and how those implementation details have changed over time.

Table of Contents

Integrating Redux with a UI

Understanding Redux Store Subscriptions

It's been said that "Redux is just a [dumb] event emitter". There's actually a fair amount of truth in that statement. Earlier MVC frameworks like Backbone would allow triggering any string as an event, and automatically trigger events like "change:firstName" in models. Redux, on the other hand, only has a single event type: "some action was dispatched".

As a reminder, here's what a miniaturized (but valid) implementation of the Redux store looks like:

function createStore(reducer) {
    var state;
    var listeners = []

    function getState() {
        return state
    }
    
    function subscribe(listener) {
        listeners.push(listener)
        return function unsubscribe() {
            var index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }
    
    function dispatch(action) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
    }

    dispatch({})

    return { dispatch, subscribe, getState }
}

Let's focus in particular on the dispatch() implementation. Notice that it doesn't check to see if the root state actually changed or not - it runs every subscriber callback after every dispatched action, regardless of whether there was any meaningful change to the state or not.

In addition, the store state is not passed to the subscriber callbacks - it's up to each subscriber to call store.getState() if desired to retrieve the latest state. (See the Redux FAQ entry on why the state isn't passed to subscribers for more details.)

The Standard UI Update Cycle

Using Redux with any UI layer requires the same consistent set of steps:

  1. Create a Redux store
  2. Subscribe to updates
  3. Inside the subscription callback:
    1. Get the current store state
    2. Extract the data needed by this piece of UI
    3. Update the UI with the data
  4. If necessary, render the UI with initial state
  5. Respond to UI inputs by dispatching Redux actions

Here's an example with a vanilla JS "counter" app, where the "state" is a single number:

// 1) Create a store
const store = createStore(counter)

// 2) Subscribe to store updates
store.subscribe(render);

const valueEl = document.getElementById('value');

// 3. When the subscription callback runs:
function render() {
    // 3.1) Get the current store state
    const state = store.getState();

    // 3.2) Extract the data you want
    const newValue = state.toString();

    // 3.3) Update the UI with the new value
    valueEl.innerHTML = newValue;
}

// 4) Display the UI with the initial store state
render();

// 5) Dispatch actions based on UI inputs
document.getElementById("increment")
    .addEventListener('click', () => {
        store.dispatch({type : "INCREMENT"});
    })

Every Redux UI integration layer is simply a fancier version of those steps.

You could do this manually, in every React, component, but it would quickly get out of hand, especially once you start trying to cut down on unnecessary UI updates.

Clearly, the process of subscribing to the store, checking for updated data, and triggering a re-render can be made more generic and reusable. That's where React-Redux and the connect API come in.

connect in a Nutshell

About a year after Redux came out, Dan Abramov wrote a gist entitled connect.js explained. It contains a miniature version of connect to illustrate how it works conceptually. It's worth pasting that here for emphasis:

// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.
function connect(mapStateToProps, mapDispatchToProps) {
  // It lets us inject component as the last step so people can use it as a decorator.
  // Generally you don't need to worry about it.
  return function (WrappedComponent) {
    // It returns a component
    return class extends React.Component {
      render() {
        return (
          // that renders your component
          <WrappedComponent
            {/* with its props  */}
            {...this.props}
            {/* and additional props calculated from Redux store */}
            {...mapStateToProps(store.getState(), this.props)}
            {...mapDispatchToProps(store.dispatch, this.props)}
          />
        )
      }
      
      componentDidMount() {
        // it remembers to subscribe to the store so it doesn't miss updates
        this.unsubscribe = store.subscribe(this.handleChange.bind(this))
      }
      
      componentWillUnmount() {
        // and unsubscribe later
        this.unsubscribe()
      }
    
      handleChange() {
        // and whenever the store state changes, it re-renders.
        this.forceUpdate()
      }
    }
  }
}

// This is not the real implementation but a mental model.
// It skips the question of where we get the "store" from (answer: `<Provider>` puts it in React context)
// and it skips any performance optimizations (real connect() makes sure we don't re-render in vain).

// The purpose of connect() is that you don't have to think about
// subscribing to the store or perf optimizations yourself, and
// instead you can specify how to get props based on Redux store state

We can see the key aspects of the API here:

  • connect is a function returning a function returning a wrapper component.
  • The wrapped components props are a combination of the wrapper component's props, the values from mapState, and the values from mapDispatch.
  • Each wrapper component is an individual subscriber to the Redux store.
  • The wrapper component abstracts away the details of which store you're using, how it's interacting with the store, and optimizing performance so that your own component only re-renders when it needs to.
  • You simply specify how to extract the data your component needs based on the store state, and the functions it can call to dispatch actions to the store

But, this miniature example hand-waves a lot of the details. In particular, as the comments point out:

  • Where does the store come from?
  • How does connect check to see if your component needs to actually update?
  • How does it implement the optimizations?

And beyond that, how did we even end up with an API that looks like this in the first place?

Development History of the React-Redux API

Early Iterations

The first few versions of Redux included React bindings as part of the main package. I won't go through those versions specifically, but you can see the changes in the release notes and READMEs for these early releases:

Fun side note: amazingly, all that iteration took place over a single week!

It took another three weeks to get up to v1.0.0-rc, which is where the meaningful history begins.

Original API Design Constraints

React-Redux was split out as a separate repo in July 2015, just before Redux v1.0.0-rc came out.

Dan filed React-Redux issue #1: Alternative API Proposals to discuss what the final API should look like. In that issue, he listed a number of design constraints that should guide how the final API worked:

Common pain points:
- Not intuitive how way to separate smart and dumb components with <Connector>, @connect
- You have to manually bind action creators with bindActionCreators helper which some don't like
- Too much nesting for small examples (<Provider>, <Connector> both need function children)

Let's go wild here. Post your alternative API suggestions.

They should satisfy the following criteria:
- Some component at the root must hold the store instance. (Akin to <Provider>)
- It should be possible to connect to state no matter how deep in the tree
- It should be possible to select the state you're interested in with a select function
- Smart / dumb components separation needs to be encouraged
- There should be one obvious way to separate smart / dumb components
- It should be obvious how to turn your functions into action creators
- Smart components should probably be able to react to updates to the state in componentDidUpdate
- Smart components' select function needs to be able to take their props into account
- Smart component should be able to do something before/after dumb component dispatches an action
- We should have shouldComponentUpdate wherever we can

These criteria form the basis for the React-Redux API that we have today, and help explain why it works the way it does.

The biggest points that came out of that discussion were using connect as a function instead of a decorator, and how to handle binding action creators.

Another fun side note: the "object shorthand" for binding action creators was one of Dan's first suggestions for the API:

Perhaps we can even go further and bind automatically if an object is passed.

Finalizing the API

The next few releases continued iterating on the connect API. Highlights of the changes:

  • v0.5.0: introduced the mapState, mapDispatch, and mergeProps arguments
  • v0.6.0: used immutability and reference checks to determine if re-rendering is necessary
  • v0.8.0: added the ability to pass store as a prop
  • v0.9.0: added the ownProps arguments to mapState/mapDispatch
  • v1.0.0: <Provider>s single child had to be a function that returned an element
  • v2.0.0: no longer supported "magically" hot reloading reducers
  • v2.1.0: added an options argument
  • v3.0.0: moved the initial mapState/mapDispatch calls to be part of the first render, to avoid state staleness issues

Continuing the observations, at one point Dan warned against connecting leaf components:

We do warn in the documentation that we encourage you to follow React flow and avoid connect()ing leaf components.

This is particularly amusing, given that we now recommend connecting components anywhere in the tree you feel it would be useful.

That brings us up to what I'd say is the "modern era" of the React-Redux API, starting with version 4.

v4.x

React-Redux v4.0.0 came out in October 2015, and had four major changes:

  • React 0.14 as a minimum and a peer dependency
  • No more React native-specific entry point
  • <Provider> no longer accepted a function as a child, but rather a standard React element
  • Refs to the child component now required the withRef option, instead of always being enabled

In addition, v4.3.0 added the "factory function" syntax of mapState/mapDispatch to allow per-component-instance memoization of selectors.

I would consider 4.x the first "complete" version of the React-Redux API. As such, it's worth digging in to some of its implementation details, as well as some common points that define the overall behavior of the API across versions.

API Behavior

Every Wrapper Component Instance is a Separate Store Subscriber

This one's pretty simple. If I have a connected list component, and it renders 10 connected list item children, that's 11 separate calls to store.subscribe(). That also means that if I have N connected components in the tree, then N subscriber callbacks are run for every dispatched action!.

Every single subscriber callback is then responsible for doing its own checks to see if that particular component actually needs to update or not, based on the store state and props.

UI Updates Require Store Immutability

We've already established that the Redux store will run all subscriber callbacks after every dispatched action, regardless of whether the state actually changed or not.

In order to implement efficient UI updates, React-Redux assumes you have updated the store state immutably, so it can use reference comparisons to determine if the state changed.

This occurs in three stages:

  1. When a connect wrapper component's subscriber callback runs, it first calls store.getState(), and checks to see if prevStoreState !== storeState. If the store state did not change by reference, then it will stop right there and bail out of any further update work, because it assumes that no other part of the store state changed.
  2. If the root state has changed, the wrapper component then runs your mapState function, and does a "shallow equality" comparison between the current result and the last result. If any of the fields have changed by reference, then your component probably needs to be updated.
  3. Assuming that there was some change in the mapState or mapDispatch results, the mergeProps() function is run to combine the stateProps from mapState, dispatchProps from mapDispatch, and ownProps from the wrapper component itself. A final check is done to see if the merged props result has changed since the last time, and if there's no change, the wrapped component will not be re-rendered.

Note: The root state comparison relies on a specific optimization in combineReducers, which checks to see if any state slices were changed while processing an action, and if not, returns the previous state object instead of a new one. This is a key reason why mutating your state results in your React UI components not updating!

The Store Instance is Passed Down via Legacy Context

In v4, rendering <Provider store={store}> puts that store instance into the legacy context API, as context.store. Any nested class component could then request that context.store be attached to the component.

The biggest reason why the entire store was put into context was because context itself was flawed. If you put an initial value into context in a parent, and some child requested that value, it would receive the correct value. However, if the parent component put an updated value by that name into context, and there was an intervening component that skipped rendering using shouldComponentUpdate -> false, the nested component would never see the updated value. That's one of the reasons why the React team always discouraged people from using legacy context directly, suggesting that any uses should be wrapped up in an abstraction so that when a "future replacement for context" came out, it would be easier to migrate. (See the React "Legacy Context" docs page and Michel Westrate's article How to Safely Use React Context for more details.)

So, one workaround was to put an event emitter instance into context, instead of an actual value. The nested components could grab the emitter instance on first render and subscribe directly, thus bypassing the sCU "roadblocks" to receive updates. React-Redux did this, as did libraries like react-broadcast.

Connected Components Accept a Store as a Prop

In addition to checking for the store instance in context, the wrapper components can optionally accept a store instance as a prop named store instead, like <MyConnectedComponent store={store}>. This works because each component subscribes to the store individually, so this component just gets that store a different way.

This is mostly useful for rendering connected components in a test without putting a <Provider> around it, but does mean you could have a connected component in the middle of your tree that uses a different store than all the other components around it.

Implementation Details

Update Logic was Directly in the Wrapper Component

Up through v4, all of the logic was part of the connect component class itself, including handling store updates, calling mapState, and determining if the wrapped component needed to re-render.

There's a couple interesting observations out of this. First, the wrapper component always called setState() whenever the root store state had changed, before it tries to do anything else.

Second, as a result, the real work of running mapState and determining if anything has changed was actually done directly in the render method.

This meant that any update to the Redux store required all wrapper components to re-render themselves in order to determine if their wrapped components actually need to update or not. That's a lot of components re-rendering every time, and also meant React was always getting called on every meaningful store update.

Child Component Rendering was Optimized Via Memoized React Elements

It's not well documented, but React has a particular performance optimization built-in. Normally, when a component renders, it creates new child elements every time:

render() {
    // Turns into: React.createElement(ChildComponent, {a : 42})
    return <ChildComponent a={42} />;
}

Every call to React.createElement() returns a new element object like {type : ChildComponent, props : {a : 42}, children : []}. So, normally every re-render results in all-new element objects being created.

However, if you return the exact same element objects as before, React will skip updating those specific elements as an optimization. (This is the basis for @babel/plugin-transform-react-constant-elements, and the behavior is discussed some in React issue #3226).

The v4 implementation takes advantage of this, by memoizing the element for the child component to skip updating it if not needed. This is necessary because the wrapper component is itself already re-rendering, so it needs some way to not have the child re-render.

v5.x

Up through v4, React-Redux was still primarily the work of Dan Abramov, albeit with a number of external contributions. Dan had given me commit rights after I wrote the Redux FAQ, and I'd spent enough time working on issues and looking the code that I felt comfortable giving more feedback beyond just how to use Redux. However, by mid 2016 Dan had joined the React team at Facebook, and was getting busy there, so he told Tim Dorr and I that we were now the primary maintainers.

About that time, a user named Jim Bolla filed an issue asking about an unusual use of connect. During the discussion, Jim commented that he was working on "an alternate version of connect", and I mentally dismissed that.

A few days after that, though, Jim filed a follow-up issue asking for feedback on his alternate implementation. We discussed some of the complexities in connect's implementation, and how those related to the use cases it was trying to solve, but I again otherwise didn't think much of it.

To my surprise, a couple days later Jim created issue #407: Completely rewrite connect() to offer advanced API, separate concerns, and (probably) resolve a lot of those pesky edge cases as a precursor to filing an actual PR. I was still really skeptical and began pointing out concerns and edge cases, but to my (pleasant) surprise, Jim kept taking my feedback and improving his WIP branch. This included producing some benchmarks which indicated that his version was noticeably faster than v4 in some particular scenarios.

Jim's efforts eventually won me over, and we began seriously collaborating on pushing his rewrite forward. That became PR #416: Rewrite connect() for better performance and extensibility .

The rewrite was released as v5.0.0 in December 2016. The biggest changes were:

  • Logic was moved from the wrapper component into memoized selectors
  • Enforceed top-down subscription updates
  • Added a new connectAdvanced API
  • More customization of comparison options
  • Overall performance improvements

All of this while keeping the same public connect API compatible with v4.

v5 also resolved a large number of existing issues as well.

There were various bugfixes up through v5.0.7, and v5.1.0 recently added support for passing React's new built-in component types like memo and lazy into connect.

Let's dig through some of the details.

API Behavior

Top-Down Updates

The connect wrapper components subscribe to the store in componentDidMount. However, because that lifecycle fires bottom-to-top in a new component tree, it was possible for child components to subscribe to the store before their parents did. Up through v4, that resulted in some nasty recurring bugs.

As an example, imagine a connected list with 10 connected list item children. If they all render right away, the list items will subscribe before the list parent. If you then delete the data for one of the items from the store, the list item component's mapState would run before the parent's did. This usually meant that the list item's mapState would throw an error and break the component tree.

v5 enforced the idea of top-down updates. Components higher in the tree always subscribe to the store before their children do. That way, in a scenario like the connected list, deleting an item from the store will result in the parent updating first, and re-rendering without that list item child before the child even has a chance to run its own mapState. This gives much more predictable behavior, and aligns with how React itself works.

We'll discuss the specifics of how this is implemented separately.

connectAdvanced

connect is fairly opinionated. It lets you extract data from the store via mapState, and prepare functions that dispatch actions via mapDispatch, but it doesn't let you use store state data in mapDispatch to prevent performance footguns. It does provide the mergeProps argument as an escape hatch, but that's separate.

However, for users that want more flexibility (such as Jim himself), v5 adds a new connectAdvanced API. Rather than taking (mapState, mapDispatch), it asks you to pass in a "selector factory". A selector instance will be created for each component and given a reference to dispatch, and the selectors will be called with (state, ownProps) on all future updates from the store or the wrapper component. That way, you can customize exactly how you want to handle derived props based on those inputs.

The original connect API is now actually implemented as a specific set of selector functions and options to connectAdvanced.

Implementation Notes

Logic is Implemented in Memoized Selectors

v5 moves all of the state derivation logic out of the wrapper component and into a separate set of homegrown memoized selector functions. These selectors specifically implement all of the connect API behavior, like:

  • Checking if the root state has changed
  • Handling the various forms of mapState and mapDispatch ( (state) vs (state, ownProps), mapDispatch as an object vs function, etc)
  • Calling mapState, mapDispatch, and mergeProps
  • Calculating the new child props and determining if a re-render is actually necessary

As a result, the subscriber callbacks can run extremely quickly, without involving React at all. In fact, React will only get involved once the wrapper component knows the child should re-render, and it uses a dummy this.setState({}) call to queue up that re-render. (We probably could have used forceUpdate() instead, but I don't think it makes any difference in this case.)

This is the biggest reason why v5 is generally faster than v4.

Custom Top-Down Subscription Logic

In order to enforce top-down subscriptions, v5 introduced a custom Subscription class. Internally, connect actually puts both the store instance and an instance of Subscription into legacy context. If no subscription exists in context, that component will subscribe to the store directly, as it must be high up in the tree. Otherwise, it subscribes to the Subscription instance. This means that each connected component is effectively subscribing to its nearest connected ancestor.

When an action is dispatched, the uppermost connected components will have their callbacks be triggered right away. If they do need to re-render, they call setState(), and wait until componentDidUpdate to trigger notification of the next tier of connected components. If no update is necessary, the next tier is notified right away.

This works, but it also requires some very tricky logic in both the Subscription class and the wrapper component itself (including dynamically adding and removing a componentDidUpdate function to micro-optimize perf).

v6.0

Motivations

v5 is great. It performs faster than v4 in almost all scenarios we've seen, and it added more flexibility.

However, the React team has continued to innovate. In particular, React 16.3 introduced the new React.createContext() API, which is an officially supported replacement for the legacy context API, and encouraged for production use. With createContext now available, they've been encouraging the community to migrate away from legacy context.

They've also been working on "concurrent React", an umbrella that describes future capabilities like "time-slicing" and "Suspense". Long-term, there are questions about how synchronous external stores like Redux will work correctly when React is running in concurrent mode.

With that in mind, we've several discussion threads about how React-Redux should work with concurrent React (#890, #950), as well as how to deal with the deprecation warnings when used in <StrictMode>

We originally planned on releasing a 5.1.0 release to fix <StrictMode> issues, but that test release turned out to be very broken. When we tried to fix the breakage, our attempts turned out to drastically hurt performance, as well as add way too much complexity.

We ultimately decided to not put out a direct fix for <StrictMode> warnings in 5.x, and instead moved on to work on v6.

The primary drivers for v6 development have been:

  • Use createContext instead of legacy context
  • Fix <StrictMode> warnings
  • Be more compatible with concurrent React behavior going forward

We went through several experimental PRs (particularly #898 and #995) before settling on PR #1000: Use React.createContext() as the best approach. Another contributor named Greg Beaver had been working with us on the <StrictMode> issues, and he and I submitted "competing" candidate PRs for v6 with varying internal implementations. His approach turned out to be slightly faster than mine, so we went with that PR, and I was then able to further optimize the PR.

API Changes

We released React-Redux v6.0.0-beta.1 just a couple weeks ago, in early November. The primary changes are:

  • Internal: Uses createContext internally instead of legacy context
  • Internal: Changes to how the components subscribe and receive the updated state from the store
  • Breaking: The withRef option has been removed in favor of using React's forwardRef capability
  • Breaking: Passing a store as a prop is no longer supported

Note that there's only two minor breaking changes to the public API!. React-Redux has a fairly comprehensive suite of unit tests for connect and <Provider>, and v6 passes the same unit tests as v5 (with appropriate changes in the tests to match some of the implementation changes). v6 should also safely run inside a <StrictMode> tag without any warnings.

Because of that, for most apps, React-Redux v6 should be a drop-in upgrade! We do require React 16.4 as a minimum because of using createContext, but other than that, most apps should be able to just bump to the new version. (Note that some community libraries may be affected by the breaking changes - again, see the v6.0.0-beta.1 release notes for details.)

However, the implementation changes do result in different behavior tradeoffs. Let's look at the changes in detail.

Implementation Notes

v6: The Store State is Passed Down via createContext

In every version up through v5.x, the Redux store instance itself was put into context, and every connected component subscribed directly. In v6, this has changed drastically.

In v6:

  • The Redux store state is put into an instance of the new createContext API
  • There is only one store subscriber: the <Provider> component

This has all kinds of ripple effects across the implementation.

It's fair to ask why we chose to change this aspect. We certainly could have put the store instance into createContext, but there's several reasons why it made sense to put the store state into context instead.

The largest reason is to improve compatibility with "concurrent React", because the entire tree will see a single consistent state value. The very short explanation on this is that React's "time-slicing" and "Suspense" features can potentially cause problems when used with external synchronous state management tools. As one example, Andrew Clark has described "tearing" as a possible problem, where different parts of the component tree see different values during the same component tree re-render pass. By passing down the current state via context, we can ensure that the entire tree sees the same state value, because React takes care of that for us.

Long-term, this should at least prevent weird bugs when React-Redux is used with concurrent-mode React. (We do have other questions we need to solve regarding how to fully make use of Suspense - I recently wrote an extensive Reddit comment describing the aspects we need to solve.)

Related to this, React-Redux has previously faced numerous problems around dispatching in constructors and componentWillMount (see some related issues ). Switching to passing the state via context should eliminate those edge cases.

Another big reason is that we get "top-down updates" for free! Context inherently propagates top-down and ties into the render process. So, if the data for a list item is deleted, the list parent will naturally re-render before the list item does. As a result, for v6 we were able to delete that custom Subscription logic - we no longer need it! That's less code that we have to maintain, and a slightly smaller package size as a result.

In addition, the original reason for passing the store instance no longer exists, because createContext correctly propagates value changes past shouldComponentUpdate blockers.

Finally, while this would have changed either way we handled the state-vs-store question, switching to createContext fixes bugs when mixing old and new context together. There's already been a number of bugs filed that indicate weird things happen if you use both forms of context in the same component, and Dan has also said that having old context being used anywhere in a component tree slows things down some.

Putting the store state into context does have some interesting implications around performance, which we'll get to in a bit.

Update Logic is Selectors, Used In Rendering

The new context API relies on a "render props" approach for receiving the values put into context, like:

<MyContext.Consumer>
{ (contextValue) => {
    // render something with the new context value here
}}
</MyContext.Consumer>

This means that context updates are directly tied to the wrapper component's render function.

v6 still uses the exact same set of selector functions for connect as v5 did. However, there's now also some additional memoization logic built into the wrapper component itself to help with the rendering process. (I initially tried adding a second inner wrapper component and doing tricks with getDerivedStateFromProps, but adding an additional selector in the one wrapper component proved to be more efficient.)

As part of that, v6 re-uses the "memoized React child element" trick to indicate that the wrapped component shouldn't be re-rendered. As with v4, this is because updates are tied to the wrapper component re-rendering, so we need a way to bail out if the child doesn't need to update. (In fact, v6 doesn't even actually implement shouldComponentUpdate, because this trick is equivalent in terms of when the child updates.)

The withRef Option is Replaced by forwardRef

One of the acknowledged downsides to Higher-Order Components is that they don't easily allow you to get access to the wrapped component inside. React 16.3 introduced a new React.forwardRef API as a solution. Libraries can use this to allow end users to put a ref on the HOC, but actually get back the real wrapped component instance.

We've added that in v6, which means that the old withRef option is no longer needed. Since this does add an additional layer of wrapping (and therefore a bit more work for React to do), it's still opt-in via the new {forwardRef : true} option.

No More store as a Prop

This is a consequence of changing from individual subscriptions per component, to a single subscription in <Provider>. Since the components no longer subscribe, passing a store directly as a prop is meaningless, and so it's been removed.

As mentioned earlier, the two main use cases for this were avoiding rendering <Provider> in tests, and allowing portions of the component tree to read from another store. The unit test use case is something that seems like it will require changes in your codebase, for those of you who are actually rendering connected components in your unit tests. For the "alternate store" use case, we've added the ability to pass your own custom context object as a prop to <Provider> and connected components, allowing them to read from a different store if desired. Hopefully that will be sufficient.

(I originally intended the API to involve passing a Context.Provider as a prop to <Provider> and passing a Context.Consumer as a prop to a connected component. However, the useContext() hook requires an entire context object, not just a consumer, despite my requests to the React team to allow it to work with just a consumer. So, if we ever switch to using hooks internally, we'd need the whole context object in the wrapper component, so best to just require that as a prop now.)

Accessing the Store via Context Has Changed

While it's never been part of our public API, it's common knowledge that any component could get a reference to the Redux store by declaring the appropriate contextTypes and using this.context.store. Many community libraries took advantage of this. For example, connected-react-router adds an extra subscription to handle location changes, while react-redux-subspace intercepts the store and passes down a wrapped-up version that presents an altered view of the state.

Obviously, this is unsupported, and any library that does this kind of thing is risking things breaking... and in v6, that all breaks because we're not using legacy context any more. However, we want to allow the community the ability to build customized solutions on top of React-Redux if desired.

Each connected component needs the current store state, and a reference to the store's dispatch function so that it can implement mapDispatch correctly. In one early PR, I had the <Provider> putting {storeState, dispatch} into context to handle that.

However, in the current v6, we actually put both the store state and the store instance into context, so the context value actually looks like {storeState, store}. That way, the components can reference store.dispatch. In addition, we're exporting our default instance of ReactReduxContext. If someone wants to, they can render that context consumer, retrieve the store instance, and do something with it.

Again, it's not an official API, but hopefully it's sufficient for the community to solve any extra use cases they might have.

Performance Implications

When we were working on the attempt to fix the initial failed 5.1.0 version, we ran some benchmarks to see how the altered version compared to 5.0.7. The large perf slowdown was a big reason why we abandoned that attempt.

In response, I set up a benchmarks repo that could compare multiple versions of React-Redux together. We used that throughout the development of v6, comparing our various WIP builds against each other and v6.

Based on those benchmarks, we expect that React-Redux v6 should be sufficiently fast enough for almost all real-world apps.

Having said that, there's some caveats.

When I first envisioned switching over to createContext, I had hoped that it would be a potential boost to performance. After all, we would be going from N subscriber calls on every action down to just 1. Unfortunately, that isn't the case.

In artificial stress test benchmarks, v6 is generally slower than v5.... but the amount varies, and the reasons are complex.

Understanding the Performance Differences

In v5, a dispatched action would result in N subscriber callbacks executing. But, thanks to the heavily memoized selector functions used with connect, only wrapper components whose data had changed would actually call this.setState() to trigger a re-render. That meant that React only got involved when updates were needed.

In v6, <Provider> has the only subscriber callback. However, in order to safely handle state changes, it immediately calls setState() using the functional updater form:

    this.unsubscribe = store.subscribe(() => {
      const newStoreState = store.getState()

      if (!this._isMounted) {
        return
      }

      this.setState(providerState => {
        // If the value is the same, skip the unnecessary state update.
        if (providerState.storeState === newStoreState) {
          return null
        }

        return { storeState: newStoreState }
      })
    })

It does try to optimize some by skipping any further work if the store state hasn't changed, but this means that React will immediately and always get involved after each dispatched action.

The next issue is that React has to trace through the component tree to find all of the matching context consumers. In a simple app structure, React would sort of do this automatically anyway, because calling setState() in the root component would recursively cause the entire component tree to be re-rendered.

However, many components in the tree may be blocking updates, whether it be manually-implemented shouldComponentUpdate -> false, instances of PureComponent or React.memo(), or connect wrappers that are skipping re-renders of their children. Let's assume for sake of the example that the topmost <App> component simply has shouldComponentUpdate -> false, thus blocking updates further down when <Provider> calls setState(). In this case, React still has to traverse the entire rendered component tree to find all the consumers.

React is fast, but that work does take time. The speed of context updates affects more than just React-Redux. The maintainer of react-beautiful-dnd opened up React issue #13739: React Context value propagation performance to discuss some of the perf implications. In that thread, Dan and Dominic suggested that the current handling of nested context updates is somewhat naive, and could potentially be optimized further down the road.

Performance Benchmarks

When I finished cleanup and optimization on the PR that became v6 beta, I did a final set of runs against our benchmarks. You can view those benchmark results here. Summarizing:

  • Both an earlier WIP v6 iteration and the final version of the v6 PR were slower than v5, in all benchmark scenarios
  • That said, the final v6 build was by far the fastest of the v6 versions
  • The amount of relative slowdown varied based on the benchmark scenario. It was most pronounced with a totally flat tree of rapidly-updating components (~20% slower), much less so with deeper trees and other update patterns (2% slower).

I'd like to re-emphasize that these are totally artificial stress-test benchmarks!. We needed some way to objectively compare different builds for performance, and so we set up scenarios that deliberately cranked up the numbers of components and frequency of dispatched actions until all the builds began slowing down.

I'd happily accept more help from the community in fleshing out the benchmarks suite, to help us come up with some more realistic scenarios. Also note that anyone ought to be able to clone the benchmarks repo, drop in a particular build of React-Redux, and replicate the approximate results on your own machine.

Summarizing Performance

As I said, I think that in real-world apps, there won't be much of a meaningful performance difference. However, that's something we need our users to test and report back to us.

It's possible that users with truly demanding performance requirements may determine that they need to stick with v5 for the time being, at least until React has made some optimizations to how context updates are handled. Or, others may decide that compatibility with async React is more important, and that they're ready to update to v6.

The Future

The existing connect API has been incredibly successful overall, and there's hundreds of thousands of apps using it. But, that doesn't mean we can't ever change it.

Alternate APIs

After v6, we're open to discussions for alternate API proposals. Many people have proposed adding a "render props" form of connect. That's certainly a possibility, as seen by the number of community-written implementations that already exist.

On the other hand, the React team's recent proposal of "Hooks" is something we can definitely make use of in React-Redux. We've already got a discussion thread about how we can eventually include a useRedux() hook, and that thread links to many user-written implementations as well. Given that hooks are being pitched as mostly eliminating the need for render props, perhaps we wouldn't need to include a render props component.

There are some issues to be solved before a useRedux() hook could work correctly with the store state in context. Right now, there's no way for a function component to stop re-rendering once it starts, and useContext() registers that component for any change to the context value. We'd need a way to stop the re-render if the derived values were the same. This is on the React team's radar, and is being discussed in in React issue #14110: Provide more ways to bail out inside Hooks.

Beyond that, perhaps there's some alternative API approach that would be easier to use and work better with concurrent React down the road, and we just haven't thought of it yet.

Using Hooks in connect

In addition to potentially adding useRedux() to the React-Redux API, we can make use of it internally. Right after hooks were announced, I created a proof-of-concept PR that uses hooks to implement connect. Hooks allowed me to drastically simplify the internal implementation of connect. In fact, the component part is so short, I'm going to paste it here:

    function createChildSelector(store) {
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }

    function ConnectFunction(props) {
      const {context, forwardRef, ...wrapperProps} = props
      const contextToRead = context || defaultContext

      const {store, storeState} = useContext(contextToRead)
      const childPropsSelector = useMemo(() =>  createChildSelector(store), [store])

      const childProps = childPropsSelector(storeState, wrapperProps)

      const renderedChild = useMemo(() => {
        return <WrappedComponent {...childProps} ref={forwardRef} />
      }, [childProps, forwardRef])

      return renderedChild
    }

(Isn't that beautiful? :) )

Once the hooks API is finalized, we can revisit this PR, and it would likely become a new 6.x point release with a minimum peer dependency on that version of React.

Additional Context Optimizations (and Magic?)

There's some hypothetical possible ways we could speed up context-related updates, possibly in conjunction with "magic" to track which pieces of state your component is actually accessing. I did an extensive writeup on this in React-Redux issue #1018: Investigate use of context + observedBits for performance optimization - see that issue if you want the nitty-gritty technical details of how I imagine this could work.

I'd certainly love to hear feedback from the community on what forms of "magic" are acceptable, especially around optimizing component updates.

Final Thoughts

Hopefully this journey through time and release notes has been informative. As you've seen, React-Redux has never been "magic" - just smart about implementing optimizations so you don't have to. For all the internal complexity, it's still just a matter of subscribing to the store, checking to see what data your component needs, and re-rendering it when necessary. The implementations have changed, but the goals haven't.

This should also help explain why you should use React-Redux instead of trying to write subscription logic yourself in your components. The Redux team has put countless hours into optimizing performance, handling edge cases, and dealing with changes in the ecosystem. You should take advantage of all the hard work that's gone into this API! :)

Again, I encourage people to try out React-Redux v6 beta (react-redux@next), and please give us feedback on how it works in your own production apps! (Note: as of this writing, we're looking at probably bumping to Release Candidate this week, and then to final if no other issues pop up.)

As always, if you've got questions, please leave a comment, file an issue, or ping me @acemarke on Reactiflux and Twitter.


This is a post in the Idiomatic Redux series. Other posts in this series:


Author Avatar

Mark Erikson

Collector of interesting links, answerer of questions