Rewrite connect() for better performance and extensibility #416

Closed
wants to merge 76 commits into
from

Conversation

@jimbolla
Contributor

jimbolla commented Jun 24, 2016

Update: Released as alpha!

See below. You can now install this as react-redux@next:

npm install --save react-redux@next

Please test it out!

TL;DR

Rewrote connect, same basic API plus advanced options/API, all tests pass, roughly 8x faster, more modular/extensible design

Overview

I rewrote connect into modular pieces because I wanted to be able to extend with custom behavior in my own projects. Now connect is a facade around connectAdvanced, by passing it a compatible selectorFactory function.

I also was able to greatly improve performance by changing the store subscriptions to execute top-down to work with React's natural flow of updates; component instances lower in the tree always get updated after those above them, avoiding unnecessary re-renders.

Design/Architecture

I split the original connect into many functions+files to compartmentalize concepts for better readability and extensibility. The important pieces:

  • components/
    • connectAdvanced.js: the HOC that connects to the store and determines when to re-render
    • Provider.js: (hasn't changed)
  • selectors/
    • connect.js: composes the other functions into a fully-compatible API, by creating a selectorFactory and options object to pass to connectAdvanced.
      that performs memoiztion and detects if the first run returns another function, indicating a factory
    • mapDispatchToProps.js: used to create a selector factory from the mapDispatchToProps parameter, to be passed to selectorFactory.js as initMapDispatchToProps. Detects whether mapDispatchToProps is missing, an object, or a function
    • mapStateToProps.js: used to create a selector factory from the mapStateToProps parameter, to be passed to selectorFactory.js as initMapStateToProps. Detects whether mapStateToProps is missing or a function
    • mergeProps.js: used to create a selector factory from the mergeProps parameter, to be passed to selectorFactory.js as initMergeProps. Detects whether mergeProps is missing or a function.
  • selectorFactory.js: given dispatch, pure, initMapStateToProps, initMapDispatchToProps, and initMergeProps, creates a connectAdvanced-compatible selector
  • wrapMapToProps.js: helper functions for wrapping values of mapStateToProps and mapDispatchToProps in compatible selector factories

  • utils/
    • Subscription.js: encapsulates the hierachial subscription concept. used by connectAdvanced.js to pass a parent's store Subscription to its children via context
    • verifyPlainObject.js: used to show a warning if mapStateToProps, mapDispatchToProps, or mergeProps returns something other than a plain object

file graph

graph

digraph files {
  "connectAdvanced.js" -> "React"
  "connectAdvanced.js" -> "Subscription.js"

  "connect.js" -> "connectAdvanced.js"
  "connect.js" -> "mapDispatchToProps.js"
  "connect.js" -> "mapStateToProps.js"
  "connect.js" -> "mergeProps.js"
  "connect.js" -> "selectorFactory.js"

  "mapDispatchToProps.js" -> "wrapMapToProps.js"
  "mapStateToProps.js" -> "wrapMapToProps.js"
}

The modular structure of all the functions in connect/ should allow greater reuse for anyone that wants to create their own connect variant. For example, one could create a variant that handles when mapStateToProps is an object by using reselect's createStructuredSelector:

customConnect.js:

import connect from 'react-redux/lib/connect/connect'
import defaultMapStateToPropsFactories from 'react-redux/lib/connect/mapStateToProps'

function createStatePropsSelectorFromObject(mapStateToProps) {
  const initSelector = () => createStructuredSelector(mapStateToProps)
  initSelector.dependsOnProps = true
  return initSelector
}

function whenStateIsObject(mapStateToProps) {
  return (mapStateToProps && typeof mapStateToProps === 'object')
    ? createStatePropsSelectorFromObject(mapStateToProps)
    : undefined 
  }
}

export default function customConnect(mapStateToProps, mapDispatchToProps, mergeProps, options) {
  return connect(mapStateToProps, mapDispatchToProps, mergeProps, {
    ...options,
    mapStateToPropsFactories: defaultMapStateFactories.concat(whenStateIsObject)
  })
}

ExampleComponent.js

// ...
export default customConnect({
  thing: thingSelector,
  thong: thongSelector
})(ExampleComponent)

And for scenarios where connect's three-function API is too constrictive, one can directly call, or build a wrapper around, connectAdvanced where they have full control over turning state + props + dispatch into a props object.

Performance

I'm using a modified version of react-redux-perf to performance test+profile the changes. It's configured to try to fire up to 200 actions per second (but becomes CPU bound), with 301 connected components. There are 2 scenarios being tested:

  • NB: a parent component with 300 child components, with no other React components between them.
  • WB: the same setup as NB but there's a "Blocker" React component between the parent and children that always returns false for shouldComponentUpdate.

I measured the milliseconds needed to render a frame using the stats.js used by react-redux-perf:

MS: avg (min-max) current NB rewrite NB current WB rewrite WB
Chrome 170 (159-223) 20 (17-55) 170 (167-231) 17 (15-59)
Firefox 370 (331-567) 20 (16-51) 430 (371-606) 19 (15-60)
IE11 270 (127-301) 40 (36-128) 300 (129-323) 33 (30-124)
Edge 240 (220-371) 37 (32-102) 260 (97-318) 28 (24-100)

On the conservitive end, the rewrite is about 8x faster under these circumstances, with Firefox even doubling that improvement. Much of the perf gains are attributed to avoiding calls to setState() after a store update unless a re-render is necessary.

In order to make that work with nested connected components, store subscriptions were changed from "sideways" to top-down; parent components always update before their child components. Connected components detected whether they are nested by looking for an object of type Subscription in the React context with the key storeSubscription. This allows Subscription objects build into a composite pattern.

After that I've used Chrome and Firefox's profilers to watch for functions that could be optimized. At this point, the most expensive method is shallowEqual, accounting for 4% and 1.5% CPU in Chrome and Firefox, respectively.

connectAdvanced(selectorFactory, options) API

In addition to the changed related to performance, the other key change is an additional API for connectAdvanced(). connectAdvanced is now the base for connect but is less opinionated about how to combine state, props, and dispatch. It makes no assumptions about defaults or intermediate memoization of results, and leaves those concerns up to the caller. It does memoize the inbound and outbound props objects. A full signature for connectAdvanced with its selectorFactory would look like:

export default connectAdvanced(
  // selectorFactory receives dispatch and lots of metadata as named parameters. Most won't be
  // useful to a direct caller, but could be useful to a library author wrapping connectAdvanced
  (dispatch, options) => (nextState, nextProps) => ({
    someProp: //...
    anotherProp: //...
  }),
  // options with their defaults:
  {
    getDisplayName: name => `ConnectAdvanced(${name})`,
    methodName: 'connectAdvanced',
    renderCountProp: undefined,
    shouldHandleStateChange: true,
    storeKey: 'store',
    withRef: false,
    // you can also add extra options that will be passed through to your selectorFactory:
    custom1: 'hey',
    fizzbuzz: fizzbuzzFunc
  }
)(MyComponent)

A simple usage may look like:

export default connectAdvanced(dispatch => {
  const bound = bindActionCreators(actionCreators, dispatch)
  return (store, props) => ({
    thing: state.things[props.thingId],
    saveThing: bound.saveThing
  })
})(MyComponent)

An example using reselect to create a bound actionCreator with a prop partially bound:

export default connectAdvanced(dispatch => {
  return createStructuredSelector({
    thing: (state, props) => state.things[props.thingId],
    saveThing: createSelector(
      (_, props) => props.thingId,
      (thingId) => inputs => dispatch(actionCreators.saveThing(thingId, inputs))
    )
  })
})(MyComponent)

An example doing custom memoization with actionCreator with a prop partially bound:

export default connectAdvanced(dispatch => {
  return createStructuredSelector({
    let thingId
    let thing
    let result
    const saveThing = inputs => dispatch(actionCreators.saveThing(thingId, inputs))

    return (nextState, nextProps) => {
      const nextThingId = nextProps.thingId
      const nextThing = nextState.things[nextThingId]
      if (nextThingId !== thingId || nextThing !== thing) {
        thingId = nextThingId
        thing = nextThing
        result = { thing, saveThing }
      }
      return result
    }
  })
})(MyComponent)

Note these are meant as examples and not necessarily "best practices."

Pros/cons

I understand there is great risk to accepting such drastic changes, that would have to be justified with significant benefits. I'll reiterate the two main benefits I believe these changes offer:

  1. Performance: There's potentially huge perf gains in situations where the number of connected components is high, stemming from conceptual changes to subscriptions so they go with the natural flow of events in React vs across them, as well as method profiling+optimizing using Chrome/FF.
  2. Extensibility/Maintainability: By splitting the responibilities of connect into many smaller functions, it should be easier both for react-redux contributors to work with the codebase and end users to extend its functionality though the additional APIs. If users can add their desired features in their own projects, that will reduce the number of feature requests to the core project.

Despite passing all the automated tests as well as week of manual testing, there is risk of impacting users dependent on implicit behavior, or that performance is worse in some unexpected circumstances. To minimize risk of impacting end users and downstream library authors, I think it would be wise to pull these changes into a "next" branch and first release an alpha package. This would give early adopters a chance to test it and provide feedback

Thanks

I'd like to thank the other github users who have so far offered feedback on these changes in #407, especially @markerikson who has gone above and beyond.

jimbolla added some commits Jun 16, 2016

Change code to make tests for factory mapStateToProps/mapDispatchToPr…
…ops pass. BREAKING CHANGE: requires setting an option parameter mapStateIsFactory/mapDispatchIsFactory to signal factory methods.
Adjust remaining 2 failing tests to account for slightly different be…
…hind-the-scenes behavior of new implementation.
Change the way nested connected components subscribe so that parent c…
…omponents always subscribe before child components, so that parents always update before children.
Fix failing tests. Change the way nested components subscribe... inst…
…ead of subscribing to the store directly they subscribe via their ancestor connected component. This ensures the ancestor gets to update before its children.
Eliminate extraneous tracking of recomputations count since we can ju…
…st compare last rendered props to new selected props
Add WrappedComponent as one of the option params passed to buildSelec…
…tor(). Useful if one wanted to build a selector from the component's attributes, such as its propTypes, for example.
@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Aug 15, 2016

Contributor

@irvinebroque : Dan wrote up a gist a while back that shows what connect() does at a conceptual level: https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e . Both the original version of connect and Jim's rewrite are way more complicated than that internally, but that gist demonstrates the overall intent: subscribe to the store, map state, merge props, render the wrapped component.

Contributor

markerikson commented Aug 15, 2016

@irvinebroque : Dan wrote up a gist a while back that shows what connect() does at a conceptual level: https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e . Both the original version of connect and Jim's rewrite are way more complicated than that internally, but that gist demonstrates the overall intent: subscribe to the store, map state, merge props, render the wrapped component.

@timdorr timdorr added this to the 5.0 milestone Aug 15, 2016

@mattkrick mattkrick referenced this pull request in ParabolInc/action Aug 17, 2016

Closed

Memoize subscription aggregators #196

@jimbolla jimbolla referenced this pull request Aug 27, 2016

Closed

v5 release chores #473

7 of 7 tasks complete
@psulek

This comment has been minimized.

Show comment
Hide comment
@psulek

psulek Sep 4, 2016

@timdorr upgrading to 5.0.0-beta.1 but i cant see exported connectAdvanced in lib. I see file src\components\connectAdvanced.js but it is not exported in index so i cant use it directly by my own. Can you add that export and publish updated beta version?

psulek commented Sep 4, 2016

@timdorr upgrading to 5.0.0-beta.1 but i cant see exported connectAdvanced in lib. I see file src\components\connectAdvanced.js but it is not exported in index so i cant use it directly by my own. Can you add that export and publish updated beta version?

@jimbolla

This comment has been minimized.

Show comment
Hide comment
@jimbolla

jimbolla Sep 5, 2016

Contributor

@psulek You can still use it directly if you do import connectAdvanced from 'react-redux/lib/components/connectAdvanced' in the meantime. If you're trying out connectAdvanced would you also review and provide feedback on the related docs in PR #480?

Contributor

jimbolla commented Sep 5, 2016

@psulek You can still use it directly if you do import connectAdvanced from 'react-redux/lib/components/connectAdvanced' in the meantime. If you're trying out connectAdvanced would you also review and provide feedback on the related docs in PR #480?

@cpsubrian

This comment has been minimized.

Show comment
Hide comment
@cpsubrian

cpsubrian Sep 8, 2016

Dumped the beta into a fairly complex single-page-app I'm working on and aside from peer-dep warnings it worked like a charm. Don't have any metrics to report but at least I can confirm that the backwards-compat appears to be working for my use-case.

Dumped the beta into a fairly complex single-page-app I'm working on and aside from peer-dep warnings it worked like a charm. Don't have any metrics to report but at least I can confirm that the backwards-compat appears to be working for my use-case.

@jimbolla jimbolla deleted the jimbolla:connect-rewrite branch Sep 14, 2016

@markerikson markerikson referenced this pull request in l2silver/react-redux-blackbox Sep 14, 2016

Open

Comparison with React-Redux v5 beta? #1

@chrisvasz

This comment has been minimized.

Show comment
Hide comment
@chrisvasz

chrisvasz Sep 28, 2016

Love this! I just dropped the beta into an existing application that uses Redux to store form information. In my application, each field is connected to the store, similar to redux-form's approach after the v6 upgrade. I loaded a fairly complex form and noticed with Perf.printWasted() that each field was performing a "wasted" render each time any other field changed. This quickly adds up to hundreds of wasted renders on a medium-sized form. (Imagine that each character in an <input> causes a state change in the store. For an average typist, that is 3-4 characters per second. Multiply that by the number of form fields and you arrive at hundreds of wasted renders per second on a form with just 25 fields, which is a small number for our use case.)

With the new react-redux, against the exact same code, the hundreds of wasted renders have dropped to zero! Love it! Way to go @jimbolla!

Love this! I just dropped the beta into an existing application that uses Redux to store form information. In my application, each field is connected to the store, similar to redux-form's approach after the v6 upgrade. I loaded a fairly complex form and noticed with Perf.printWasted() that each field was performing a "wasted" render each time any other field changed. This quickly adds up to hundreds of wasted renders on a medium-sized form. (Imagine that each character in an <input> causes a state change in the store. For an average typist, that is 3-4 characters per second. Multiply that by the number of form fields and you arrive at hundreds of wasted renders per second on a form with just 25 fields, which is a small number for our use case.)

With the new react-redux, against the exact same code, the hundreds of wasted renders have dropped to zero! Love it! Way to go @jimbolla!

@Sinistralis

This comment has been minimized.

Show comment
Hide comment
@Sinistralis

Sinistralis Sep 30, 2016

Dropped this into the same application I tested earlier. This test was performed on mock data, so the data is slightly different but the structure, relationship, etc of the data are all the same across both.

The test was a drag and drop implementation using lists and smart components. The difference with this change is MASSIVE in terms of render count. (This is on Chrome, which seems to be the browser best able to handle this scenario. Mobile fares much, much worse)

Before:
capture

After:
capture2

Sinistralis commented Sep 30, 2016

Dropped this into the same application I tested earlier. This test was performed on mock data, so the data is slightly different but the structure, relationship, etc of the data are all the same across both.

The test was a drag and drop implementation using lists and smart components. The difference with this change is MASSIVE in terms of render count. (This is on Chrome, which seems to be the browser best able to handle this scenario. Mobile fares much, much worse)

Before:
capture

After:
capture2

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Oct 1, 2016

Hi,

This is good news, but can someone explain the performance improvements? I'm not sure to understand why it performs better than before as a drop-in replacement

slorber commented Oct 1, 2016

Hi,

This is good news, but can someone explain the performance improvements? I'm not sure to understand why it performs better than before as a drop-in replacement

@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Oct 1, 2016

Contributor

@jimbolla can explain further, but it's a combination of several factors. Primarily, there's a ton of custom memoization going on using selectors, which avoids the need to call setState. v4, on the other hand, called setState and then did the real checking in the wrapper's render function. It also ensures that subscriptions are handled top-down, fixing a loophole where children could get notified by the store before ancestors and causing unnecessary render checks. See the rest of this thread, as well as #407 for discussion and details.

Contributor

markerikson commented Oct 1, 2016

@jimbolla can explain further, but it's a combination of several factors. Primarily, there's a ton of custom memoization going on using selectors, which avoids the need to call setState. v4, on the other hand, called setState and then did the real checking in the wrapper's render function. It also ensures that subscriptions are handled top-down, fixing a loophole where children could get notified by the store before ancestors and causing unnecessary render checks. See the rest of this thread, as well as #407 for discussion and details.

@jimbolla

This comment has been minimized.

Show comment
Hide comment
@jimbolla

jimbolla Oct 1, 2016

Contributor

@slorber @markerikson The short short version... Forcing the order of components' store subscriptions to trigger top-down allows connect to avoid extra calls to setState, render, and mapStateToProps'n'friends.

Contributor

jimbolla commented Oct 1, 2016

@slorber @markerikson The short short version... Forcing the order of components' store subscriptions to trigger top-down allows connect to avoid extra calls to setState, render, and mapStateToProps'n'friends.

@Sinistralis

This comment has been minimized.

Show comment
Hide comment
@Sinistralis

Sinistralis Oct 1, 2016

Doesn't that mean it also fixes the issue where can you end up getting components in in-determinant states because they render before their parent, which could result in a child trying to access non-existent data because they haven't been unmounted yet?

I've actually run into this problem, and tested to see if the issue still occurs with v5 and it does not, so another issue fixed?

Doesn't that mean it also fixes the issue where can you end up getting components in in-determinant states because they render before their parent, which could result in a child trying to access non-existent data because they haven't been unmounted yet?

I've actually run into this problem, and tested to see if the issue still occurs with v5 and it does not, so another issue fixed?

@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Oct 1, 2016

Contributor

Y'know, that's a good point. I've seen that in my own prototype app, where I frequently use connected parents rendering connected list items. I bet there's a good chance v5 resolves that.

Contributor

markerikson commented Oct 1, 2016

Y'know, that's a good point. I've seen that in my own prototype app, where I frequently use connected parents rendering connected list items. I bet there's a good chance v5 resolves that.

@jimbolla

This comment has been minimized.

Show comment
Hide comment
@jimbolla

jimbolla Oct 1, 2016

Contributor

Yep. That was actually the motivation for making the change. The perf was a nice surprise.

Contributor

jimbolla commented Oct 1, 2016

Yep. That was actually the motivation for making the change. The perf was a nice surprise.

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Oct 3, 2016

That's nice! so finally it somehow breaks compatibility (not in a way that's likely to break someone's app) and the new behavior makes connect more performant, in addition to being more flexible.

However @jimbolla can you take a look at this usecase of a list of 100k connected elements? http://stackoverflow.com/a/34788570/82609
I've seen your comments pointing out that the new implementation will permit to solve this problem in an efficient way, but I'm not sure how it can be done. Any code to share?

slorber commented Oct 3, 2016

That's nice! so finally it somehow breaks compatibility (not in a way that's likely to break someone's app) and the new behavior makes connect more performant, in addition to being more flexible.

However @jimbolla can you take a look at this usecase of a list of 100k connected elements? http://stackoverflow.com/a/34788570/82609
I've seen your comments pointing out that the new implementation will permit to solve this problem in an efficient way, but I'm not sure how it can be done. Any code to share?

@jimbolla

This comment has been minimized.

Show comment
Hide comment
@jimbolla

jimbolla Oct 4, 2016

Contributor

@slorber A few thoughts on that:

  • Can squeeze some extra perf by using connectAdvanced with a custom selector instead of connect. It would probably be worth it at 100k connected components.
  • Another option besides those you suggested would be grouping your components into pages of (let's say) 100, so instead of 100k connected components, you'd have 1000 connected pages containing 100 unconnected components. Not sure if this would be faster or not. But it's worth experimenting
  • In v5, connect/connectAdvanced take an option shouldHandleStateChanges. (connect sets it based on whether mapStateToProps is provided, connectAdvanced defaults it to true) You could connect a component that reads from state, but doesn't subscribe to store changes. This means it would only trigger rerenders when it receives new props from above. This may be be faster or slower.
Contributor

jimbolla commented Oct 4, 2016

@slorber A few thoughts on that:

  • Can squeeze some extra perf by using connectAdvanced with a custom selector instead of connect. It would probably be worth it at 100k connected components.
  • Another option besides those you suggested would be grouping your components into pages of (let's say) 100, so instead of 100k connected components, you'd have 1000 connected pages containing 100 unconnected components. Not sure if this would be faster or not. But it's worth experimenting
  • In v5, connect/connectAdvanced take an option shouldHandleStateChanges. (connect sets it based on whether mapStateToProps is provided, connectAdvanced defaults it to true) You could connect a component that reads from state, but doesn't subscribe to store changes. This means it would only trigger rerenders when it receives new props from above. This may be be faster or slower.
@jide

This comment has been minimized.

Show comment
Hide comment
@jide

jide Oct 5, 2016

Hi !

Since I updated a project to react-redux 5.0.0-beta.2, I see this error popping up, any clue ?
capture d ecran 2016-10-05 a 15 43 28
Which comes from :

      if (typeof ownProps[key] !== 'undefined') {
        (0, _warning2.default)(false, 'Duplicate key ' + key + ' sent from both parent and state');
        break;
      }

jide commented Oct 5, 2016

Hi !

Since I updated a project to react-redux 5.0.0-beta.2, I see this error popping up, any clue ?
capture d ecran 2016-10-05 a 15 43 28
Which comes from :

      if (typeof ownProps[key] !== 'undefined') {
        (0, _warning2.default)(false, 'Duplicate key ' + key + ' sent from both parent and state');
        break;
      }
@timdorr

This comment has been minimized.

Show comment
Hide comment
@timdorr

timdorr Oct 5, 2016

Member

@jide Looks like we're calling warning() incorrectly in that commit. I'll fix and push out another beta.

Member

timdorr commented Oct 5, 2016

@jide Looks like we're calling warning() incorrectly in that commit. I'll fix and push out another beta.

@jide

This comment has been minimized.

Show comment
Hide comment
@jide

jide Oct 5, 2016

Ok thanx !

jide commented Oct 5, 2016

Ok thanx !

@timdorr

This comment has been minimized.

Show comment
Hide comment
@timdorr

timdorr Oct 5, 2016

Member

OK, pushed beta.3 to remove that check for now.

Member

timdorr commented Oct 5, 2016

OK, pushed beta.3 to remove that check for now.

clbn added a commit to clbn/freefeed-gamma that referenced this pull request Dec 4, 2016

[components-rewiring] PostComment: fix deleting comments
On deleting comments, the app might want to re-render already removed
PostComment before updating PostComments (which would prevent that
by removing that comment ID from the list in the first place).

Actually, it will be fixed by migrating to react-redux@5, since that
version will be forcing the order of components' store subscriptions to
trigger top-down (see
reduxjs/react-redux#416 (comment)
plus next three comments). However, we cannot just sit and wait for
stable react-redux@5, right?

N.B. It's part of [components-rewiring], since the issue with race
condition was introduced by PostComment rewiring (connecting directly
to the store) in one of previous changes.
- return false
- }
+ for (let key in b) {
+ if (hasOwn.call(b, key)) countB++

This comment has been minimized.

@jcready

jcready Dec 14, 2016

This could exit before completely iterating through all the b keys by checking if countA is less than countB in each iteration:

for (let key in b) {
  if (hasOwn.call(b, key) && countA < ++countB) return false
}

return true
@jcready

jcready Dec 14, 2016

This could exit before completely iterating through all the b keys by checking if countA is less than countB in each iteration:

for (let key in b) {
  if (hasOwn.call(b, key) && countA < ++countB) return false
}

return true

This comment has been minimized.

@timdorr

timdorr Dec 14, 2016

Member

A PR to change that out would be appreciated!

@timdorr

timdorr Dec 14, 2016

Member

A PR to change that out would be appreciated!

@natevw

This comment has been minimized.

Show comment
Hide comment
@natevw

natevw Jan 27, 2017

Looks like this has been released. Updating to 5.0.4 fixed some seemingly-bizarre bugs (caused by a previously-incorrect assumption that props were reliably passed from the top down), and haven't noticed any regressions. Thanks for making this happen!

natevw commented Jan 27, 2017

Looks like this has been released. Updating to 5.0.4 fixed some seemingly-bizarre bugs (caused by a previously-incorrect assumption that props were reliably passed from the top down), and haven't noticed any regressions. Thanks for making this happen!

@mjrussell mjrussell referenced this pull request in mjrussell/redux-auth-wrapper Mar 29, 2017

Closed

Redux auth wrapper/redux-thunk combo doesn't respect hierarchy #101

@markerikson markerikson referenced this pull request in coderitual/redux-performance Apr 2, 2017

Open

Goals for this project? #1

@apechimp apechimp referenced this pull request in FormidableLabs/redux-little-router May 27, 2017

Closed

Rendering after back/forward inconsistent with push #144

@ashtonsix ashtonsix referenced this pull request in beyond-labs/react-mirror Jun 4, 2017

Closed

Call `this.setState` "top-down" #4

@renovate renovate bot referenced this pull request in JimmyLv/Haiku Feb 2, 2018

Open

Update dependency react-redux to v5 #33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment