reduxjs / react-redux

Official React bindings for Redux

Home Page:https://react-redux.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React-Redux Roadmap: v6, Context, Subscriptions, and Hooks

markerikson opened this issue · comments

Update: React-Redux v7 final is now available!

We've just published v7 final, with the changes covered in this thread. Upgrade your apps today!

Back in December, we released React-Redux version 6.0. This release featured a major rearchitecture of the implementations for <Provider> and connect, centered around our use of React's context API.

We put the pre-release builds through extensive tests, including our existing unit test suite and multiple performance benchmark scenarios. We also requested early adopter feedback on beta versions. Based on all that information, we determined that v6 was ready for public release, and published it.

I think most library maintainers would agree that the best way to guarantee getting bug reports is to publish a new major version, because A) that means the entire community is about to start using the new code, and B) it's impossible for a maintainer to anticipate every way that the library is being used. That's definitely been true with React-Redux version 6.

Version 6 does work as intended, and many of our users have successfully upgraded from v5 to v6, or started new projects with v6. However, we've seen a number of concerns and challenges that have come up since v6 was released. These relate to both the current behavior of v6 and its API, as well as future potential changes such as the ability to ship official React hooks that use Redux.

In this issue, I want to lay out a description of the changes we made in v6 and why we made them, the challenges we are currently facing, the constraints that potential solutions need to conform to, and some possible courses of action for resolving those challenges.

TL;DR:

  • v6 switched from direct store subscriptions in components, to propagating store state updates via createContext
  • It works, but not as well as we'd hoped
  • React doesn't currently offer the primitives we need to ship useRedux() hooks that rely on context
  • Based on guidance from the React team, we're going to switch back to using direct subscriptions instead, but need to investigate an updated way to re-implement that behavior
  • As part of that, we need to improve our tests and benchmarks to ensure we're covering more use cases from throughout the community
  • Our ability to ship public hooks-based APIs depends on switching back to subscriptions, and how we update connect may require a major version bump.
  • We need volunteers and contributions from the React-Redux community to make all this happen!

Implementation Changes in Version 6

In my blog post The History and Implementation of React-Redux, I described the technical changes we made from v5 to v6, and why we made them. Summarizing those changes:

  • v5 and earlier:
    • The store instance was put into legacy context
    • Each connected component instance was a separate subscriber to the Redux store
    • Because components subscribed directly, they could also accept a store instance as a prop named store, and use that instead of the instance from context
  • In v6:
    • The current Redux store state is propagated via React's new createContext API
    • Only <Provider> subscribes to the store - the components just read the store state from context
    • Since components don't subscribe directly, passing store as a prop is meaningless and was removed

We made these changes for several reasons:

  • Legacy context will eventually be removed, and can cause problems when used side-by-side with the new context API
  • The React team has warned that React's upcoming "Concurrent Mode" and "Suspense" capabilities will cause many existing state patterns to break or behave unpredictably. By putting the store state itself into context, React ensures the entire component tree sees the same state value consistently, and therefore React-Redux would hopefully would be more compatible with Concurrent Mode when it comes out.
  • v5 had to specifically implement custom tiered subscription logic to enforce top-down updates in order to fix "zombie child" bugs (where a child component subscribes before its parent, has its data deleted from the store, and then tries to read that data before the parent stops rendering it). Context updates top-down by default as React renders, allowing us to remove our own custom code for this issue.

Challenges with v6

Performance

During the development of v6, we put together a performance benchmarks suite that we could use to compare the behavior of different builds of React-Redux. These benchmarks are artificial stress tests that don't necessarily match real-world app setups, but they at least provide some consistent objective numbers that can be used to compare the overall behavior of builds to keep us from accidentally shipping a major performance regression.

Our comparisons showed that v6 was generally slower than v5 by different amounts in different scenarios, but we concluded that real-world apps would probably not experience any meaningful differences. That seems to have been true for most users, but we have had several reports of performance decreases in some apps.

Fundamentally, this is due to how v6 relies on context for propagating state updates. In v5, each component could run its own mapState function, check the results, and only call this.setState() if it knew it needed to re-render. That meant React only got involved if a re-render was truly necessary. In v6, every Redux state update immediately causes a setState() in <Provider> at the root of the tree, and React always has to walk through the component tree to find any connected components that may be interested in the new state. This means that v6 ultimately results in more work being done for each Redux store update. For usage scenarios with frequent Redux store updates, this could result in potential slowdowns.

store as Prop

We removed the ability to pass a prop named store to connected components specifically because they no longer subscribe to stores directly. This feature had two primary use cases:

  • Passing store instances to connected components in tests without needing to wrap them in a <Provider>
  • Rare situations where a specific component needed to subscribe to a different store than the rest of the application component tree around it

Our general guidance for the first use case was to always wrap components in <Provider> in tests. We tried to provide a solution for the second use case by allowing users to pass custom context instances to both <Provider> and connected components.

However, since the release of v6, we've had several users express concerns that the removal of store as a prop breaks their tests, and that there are specific problems with trying to use the combination of Enzyme's shallow() function with a <Provider> (and React's new context API in general).

Context API Limitations

At first glance, React's new createContext API appears to be perfectly suited for the needs of a library like React-Redux. It's built into React, it was described as "production-ready" when it was released, it's designed for making values available to deeply-nested components in the tree, and React handles ordering the updates in a top-down sequence. The <Context.Provider> usage even looks very similar to React-Redux's <Provider>.

Unfortunately, further usage has shown that context is not as well suited for our use case as we first thought. To specifically quote Sebastian Markbage:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

React-Redux and Hooks

In addition to concerns about performance and state update behaviors, the initial release of the React Hooks API will not include a way to bail out of updates triggered by a context value update. This effectively blocks React-Redux from being able to ship some form of a useRedux() hook based on our current v6 implementation.

To again quote Sebastian:

I realize that you've been put in a particularly bad spot because of discussions with our team about concurrent mode. I apologize for that. We've leaned on the use of context for propagation which lead you down the route of react-redux v6. I think it's generally the right direction but it might be a bit premature. Most existing solutions won't have trouble migrating to hooks since they'll just keep relying on subscriptions and won't be concurrent mode compatible anyway. Since you're on the bleeding edge, you're stuck in a bad combination of constraints for this first release. Hooks might be missing the bailout of context that you had in classes and you've already switched off subscriptions. :(

I'll pull out one specific sentence there for emphasis:

I think [v6's use of context is] generally the right direction but it might be a bit premature.

Constraints

Whatever solutions we come up with for these challenges need to fit within a variety of overlapping constraints.

Performance Should Match or Improve vs v5

Ideally, v6 should be at least as fast as v5, if not faster. "Faster", of course, is entirely dependent on what metrics we're measuring, and how we're measuring them.

Handle Use Cases for store as Prop

We need to support the use cases that were previously handled by passing a store directly as a prop to connected components. As part of that, we should ensure that we have tests that cover these usages.

Future React Compatibility

We have some idea what the potential concerns are around React's future Concurrent Mode and Suspense capabilities, but it would help to have some concrete examples that we can use to ensure we're either not breaking application behavior, or at least can help us quantify what the potential breakages are.

Quoting Dan Abramov:

We need to be clear that there are several “levels” of compatibility with new features of React. They’re not formalized anywhere yet but a rough sketch for a library or technique X could be:

  1. X breaks in sync mode
  2. X works in sync mode but breaks in concurrent mode
  3. X works in concurrent mode but limits its DX and UX benefits for the whole app
  4. X works in concurrent mode but limits its UX benefits for the whole app
  5. X works in concurrent mode but limits its DX and UX benefits for features written in X
  6. X works in concurrent mode but limits its UX benefits for features written in X
  7. X works in concurrent mode and lets its users take full advantage of its benefits

This is not a strict progression and there’s a spectrum of tradeoffs. (For example, maybe there is some temporary visual inconsistencies such as different like counts, but no crashes are guaranteed.)

But we need to be more precise about where React Redux is, and where it aims to be.

At a minimum, we should ensure that React-Redux does not cause any warnings when used inside a <StrictMode> component. That includes use of semi-deprecated lifecycle methods like componentWillReceiveProps (which was used in v5).

Don't Re-Introduce "Zombie Child" Problems

Up through v4, we had reports of a bug that could happen when children subscribed before parents. At a technical level, the actual issue was:

  • combining stale ownProps with new state in mapStateToProps()
  • running mapStateToProps() for a component that will be unmounted later in the overall render cycle, combined with a failure to handle cases where the values needed from the store might not exist

As an example, this could happen if:

  • A connected list immediately rendered connected list items
  • The list items subscribed before the parent list
  • The data for a list item was deleted from the store
  • The list item's mapState then ran before the parent had a chance to re-render without that child
  • The mapState tried to read nested state without safely checking to see if that data existed first

v5 specifically introduced an internal Subscription class that caused connected components to update in a tiered approach, so that parents always updated before children. We removed that code in v6, because context updates top-down already, so we didn't need to do it ourselves.

Whatever solutions we come up with should avoid re-introducing this issue.

Allow Shipping Redux Hooks

The React community as a whole is eagerly awaiting the final public release of React Hooks, and the React-Redux community is no exception. Redux users have already created a multitude of unofficial "Redux hooks" implementations, and have expressed a great deal of interest in an official set of hooks-based APIs as part of React-Redux.

We absolutely want to ship our own official Redux hooks as soon as possible. Whatever changes we decide on need to make that feasible.

Potentially Use Hooks in connect

When hooks were announced, I immediately prototyped a proof of concept that reimplemented connect using hooks internally. That simplified the connect implementation dramatically.

I'd love to use hooks inside React-Redux itself, but that would require bumping our peer dependency on React from the current value of 16.4 in v6, to a minimum of 16.8. That would require a corresponding major version bump of React-Redux to v7.

That's a potential option, but I'd prefer not to bump our own major version if we can avoid it. It should be possible to ship a useRedux() as part of our public API as a minor 6.x version, and leave it up to the user to make sure they've got a hooks-capable version of React if they want to import that hook. Then again, it's also possible that a hooks-based version of connect would be necessary to solve the other constraints.

Continue to Work with Other Use Cases

The broader React-Redux ecosystem has updated itself to work with v6. Some libraries have had to change from using the withRef option to forwardRef. Other libraries that were accessing the store out of the (undocumented private) legacy context have switched to accessing the store out of our (still private) ReactReduxContext instance.

This has also brought up other semi-niche use cases that we want to support, including having connect work with React-Hot-Loader, and support for dynamically updating reducers and store state in SSR and code-splitting scenarios.

The React-Redux community has built some great things on top of our baseline capabilities, and we want to allow people to continue to do so even if we don't explicitly support everything ourselves.

Courses of Action

So here's where the rubber meets the road.

At the moment, we don't have specific implementations and solutions for all those constraints. We do have some general outlines for some tasks and courses of action that will hopefully lead us towards some working solutions.

Switch connect Back to Direct Subscriptions

Based on guidance from the React team, the primary thing we should do at this point is switch connect back to using direct subscriptions.

Unfortunately, we can't just copy the v5 implementation directly into the v6 codebase and go with it as-is. Even ignoring the switch from legacy context to new context, v5 relied on running memoized selectors in componentWillReceiveProps to handle changes to incoming props, and then returning false in shouldComponentUpdate if necessary. That caused warnings in <StrictMode>, which we want to avoid.

We need to design a new internal implementation for store subscription handling that satisfies the listed constraints. We've done some early experiments, but don't have any specific approaches yet that we can say are the "right" way to do it.

We do actually already put the store instance into createContext, so nothing needs to change there. The specific values we put into context are not considered part of our public API, so we can safely remove the storeState field from context.

Bringing back direct subscriptions does mean that we can probably bring back the ability to pass store as a prop directly to connected components. That should hopefully resolve the concerns about testing and isolated alternate-store usage, because it's the same API that solved those use cases previously.

Expand Test Suite for More Use Cases

We currently have a fairly extensive unit test suite for connect and <Provider>. Given the discussions and issues we're facing, I think we need to expand that suite to make sure we're better covering the variety of use cases the community has. For example, I'd like to see some tests that do Enzyme shallow rendering of connected components, mock (or actual) SSR and dynamic loading of slice reducers, and hopefully tests or apps that show the actual problems we might face in a Concurrent Mode or Suspense environment.

Consider Marking Current Implementation as Experimental

The v6 implementation does work, and there may be people who prefer to use it. Rather than just throw it away, we could potentially keep it around as a separate entry point, like import {connect, Provider} from "react-redux/experimental".

Improve Benchmarks Suite

Our current benchmarks are somewhat rudimentary:

  • They only measure raw page FPS, no other metrics
  • Measuring FPS requires cranking up the number of connected components and update frequency to arbitrarily high levels until the FPS starts to drop below 60
  • The current benchmark scenarios are sorta meant to represent certain use cases, but are probably not good representations of real-life behavior

I would really like our benchmarks to be improved in several ways:

  • Capture more metrics, like time to mount a component tree, time to complete a single render pass for an entire tree, and breaking down the time spent in different aspects of a single update cycle (running the reducer, notifying subscribers, running mapState functions, queuing updates, wrappers re-rendering, etc).
  • We should see how the benchmarks behave when used with ReactDOM's unstable_batchedUpdates API, to see how much of a difference that makes in overall performance
  • Our benchmarks currently only include web usage. I would like to be have some React Native benchmarks as well.
  • We should have additional benchmark scenarios for more use cases, like apps with connected forms, a large tree of unconnected components with only leaf components being connected, etc.

In general, we need to better capture real-world behavior.

Officially Support Batched React Updates

ReactDOM has long included an unstable_batchedUpdates API. Internally, it uses this to wrap all event handlers, which is why multiple setState() calls in a single event handler get batched into a single update.

Although this API is still labeled as "unstable", the React team has encouraged us to ship an abstraction over this function officially as part of React-Redux. We would likely do this in two ways:

  • Export a function called batch that can be used directly by end users themselves, such as wrapping multiple dispatches in a thunk
  • Export a ReactReduxEnhancer that would wrap dispatches in unstable_batchedUpdates, similar to how tappleby/redux-batched-subscribe works.

It's not yet clear how much this would improve overall performance. However, this may hopefully act as a solution to the "zombie child component" problem. We need to investigate this further, but the React team has suggested that this would be a potential solution.

Currently, ReactDOM and React Native apparently both separately export their own versions of unstable_batchedUpdates, because this is a reconciler-level API. Since React-Redux can be used in either environment, we need to provide some platform abstraction that can determine which environment is being used, and import the method appropriately before re-exporting it. This may be doable with some kind of .native.js file that is picked up by the RN build system. We might also need a fallback in case React-Redux is being used with some other environment.

Hooks

We can't create useRedux() hooks for React-Redux that work correctly with v6's context-based state propagation, because we can't bail out of updates if the store state changed but the mapState results were the same. However, the community has already created numerous third-party hooks that rely on direct store subscriptions, so we know that works in general. So, our ability to ship official hooks is dependent on us first switching back to direct store subscriptions in connect, because both connect and any official hooks implementation need to share the same state propagation approach.

There's a lot of bikeshedding that can be done about the exact form and behavior of the hooks we should ship. The obvious form would be to have a direct equivalent of connect, like useRedux(mapState, mapDispatch). It would also be reasonable to have separate hooks like useMapState() and useMapDispatch(). Given the plethora of existing third-party hooks libs, we can survey those for API and implementation ideas to help determine the exact final APIs we want to ship.

In theory, we ought to be able to ship these hooks as part of a 6.x minor release, without requiring that you have a minimum hooks-capable version of React. That way, users who are still on React <= 16.7 could conceivably use React-Redux 6.x, and it will work fine as long as they don't try to actually use the hooks we export.

Long-term, I'd probably like to rework connect to be implemented using hooks internally, but that does require a minimum peer dependency of React 16.8. That would be a breaking change and require a major version bump for React-Redux. I'd like to avoid that on general principle. But, if it turns out that a hooks-based connect implementation is actually the only real way to satisfy the other constraints, that may turn out to be necessary.

Requests for Community Help

There's a lot of stuff in that list. We need YOUR help to make sure React-Redux works well for everyone!

Here's how you can help:

  • Help us come up with the right approach for using direct subscriptions in connect, including experimenting with implementations yourself, and giving us feedback on any test releases we publish.
  • Improve our benchmarks suite and test suite by describing use cases that aren't currently covered, adding new tests and benchmarks to cover additional scenarios, and updating the benchmarks suite to capture additional metrics
  • We need to determine exactly what limitations React-Redux faces with Concurrent React. The React team has some rough examples of concurrent React usage - porting those to use Redux would help show what specific problems might happen.
  • Implement our support for batched updates, and make sure it works in different environments.
  • Discuss what our future hooks-based APIs should look like, including rounding up the existing third-party libs to help guide the API design.

We can start with some initial discussion in this issue, but I'll probably try to open up some specific issues for these different aspects in the near future to divide up discussion appropriately.

Final Thoughts

There's a lot of great things ahead for React. I want to make sure that React and Redux continue to be a great choice for building applications together, and that Redux users can take advantage of whatever capabilities React offers if at all possible.

The React-Redux community is large, growing, smart, and motivated. I'm looking forward to seeing how we can solve these challenges, together!

I'm not really sure the batchedUpdates strategy alone can deal with the update bailout performance issue.

It would allow react-redux to move the bailout to the shouldComponentUpdate phase instead of having connect() return memoized children to skip child reconciliation, but it'd still trigger a render pass on every single connect() HOC and a full React root traversal (even if it is 100% bailouts), so I estimate the cost to only be marginally smaller than v6.0's approach. It would allow for a potential hooks API to bail out, though, which would be a usability gain at least.

I still have some ideas for things to try without breaking API, though.

I'm open to experimenting with as many variations on implementation approaches as we can come up with :) All the more reason to have additional tests that can quantify and express the additional constraints we need to meet, beyond what we have in the repo now.

Bringing back direct subscriptions does mean that we can probably bring back the ability to pass store as a prop directly to connected components. That should hopefully resolve the concerns about testing and isolated alternate-store usage, because it's the same API that solved those use cases previously

Thank you!!

commented

FWIW I don’t see anything preventing us from adding provider support to shallow renderer. If that’s the concern about testing.

commented

@Jessidhia The batchedUpdates thing is more about fixing the “zombie child”. It’s the idiomatic React solution for always traversing from the top. It can also improve perf outside event handlers but avoiding the bad ordering is the main motivation.

Please don't forget to support immutable data structure libraries like immutable-js.

They have a special equality check like is() and we need a way to define a custom comparison function for connect or useRedux.

see facebook/react#14569 (comment)

This is a fantastic writeup, sincerely thank you so much for your hard work @markerikson! 🙌

The specific values we put into context are not considered part of our public API, so we can safely remove the storeState field from context.

I do agree with this. That said, this will technically break some implementations of dynamically injected reducers in v6 since they currently have to rely on those private APIs, so I was glad to see you explicitly mention that issue. 😀 I don't think any of the "big ones" are using storeState for their implementation currently though (at least not dynostore or dynamic-modules I think) so shouldn't be a huge issue.

I really agree with a test-based approach here, if I can find the time I'll try to contribute some tests focusing on SSR.

Just wanted to say I'm excited about this roadmap and feel it's the correct direction forward until both context performance for large stores with frequent updates and bailout of useContext is added/supported. I'd love to see a compilation of all the hooks approaches the community has developed thus far and review in a centralized place! Awesome work @markerikson , the community greatly appreciated your work!

@vincentjames501 : yup, agreed.

A couple months ago, @adamkleingit put together this list of existing Redux hooks libs:

https://twitter.com/adamklein500/status/1072457324932067329

Collating that list of existing Redux hooks libs is absolutely something that someone else besides me can do, and would really help (like, a Google Docs spreadsheet or something like that). Links, summaries, API comparisons, etc.

Others not in that list as I run across them:

https://github.com/animify/useRestate

It's really interesting to me that you mention using setState batching as a potential solution because that's the way I originally solved the zombie/tearing issue for react-redux in the #99 PR. I guess things can come around sometimes :)

That PR didn't actually use the unstable_batchedUpdates itself but one could enable it via the redux-batched-updates middleware in order to avoid the zombie child issue.


In the @gaearon's "React as a UI Runtime" article he explains how and why React uses batching for event handlers

https://overreacted.io/react-as-a-ui-runtime/#batching

This really resonates with me because the reason for it seems to be basically same as why Redux would need it.

And using it the issue is pretty easy to solve. Inspired by this I created my own redux-hooks implementation that does not suffer from the zombie issue:

https://github.com/epeli/redux-hooks

It works by using static context which contains the store reference and an array reference. When the useReduxState() is called it appends an update() function into the array by mutating it (context values are never updated in this implementation). Only the <HooksProvider> subscribes to the store. On a store update it just iterates over the update function and calls them inside the unstable_batchedUpdates callback:

https://github.com/epeli/redux-hooks/blob/f26938076f4a253a83fef4823cfbcd5b2f52fc01/src/redux-hooks.tsx#L26-L30

Here's a simple todoapp using this implementation

Codesandbox: https://codesandbox.io/s/github/epeli/typescript-redux-todoapp/tree/hooks
Github: https://github.com/epeli/typescript-redux-todoapp/tree/hooks

I really cannot comment about the performance of this since I just put it together but I don't think there's any reason why it couldn't be good. This implementation now just does the basic performance optimization by bailing out if mapState does not produce new value (shallow equal check).

I'd be super interested hearing any feedback on this. Even just for the internal hooks usage. This is the first thing I've ever written with hooks.


EDIT: Oh, and this not a hooks specific way to implement this. The same method would work for the regular HOC connect(). I just wanted to play with hooks a bit (and I don't think there are any other zombie free implementations of redux hooks out there).

@markerikson ❤️

To weigh in slightly: for react-beautiful-dnd we have not moved to react-redux v6 for performance reasons https://david-dm.org/atlassian/react-beautiful-dnd

Here was my initial performance investigations of the new context api for propigating updates: facebook/react#13739

Hi everyone!

I just migrated one of my libs (which has a pretty extensive test coverage) to use unstable_batchedUpdates and I can confirm two things.

  • Exposing two builds - one .js and one .native.js - and adding the .js build as the main to package.json does not work for react-native. Exposing a single build with extra platform chunks (platform.js and platform.native.js) and requiring platform from the main chunk does work for both react-dom and react-native however. You can check the implementation here. (Check the src and the platforms folders and the scripts/build.js file.)

  • unstable_batchedUpdates does solve the zombie child issues. I used to have extra logic to reorder setState calls in a single sync batch to guarantee a parent-child order in the renders. I could totally remove this after I wrapped sync batches with unstable_batchedUpdates which seems to order re-renders in a parent-child order even if the setState calls are in a different order (subscription order I assume).

I hope these help a bit.

I've done some early experimenting the last couple evenings with trying to rework the guts of connect. My utterly hacked-up WIP experimental code is here:

https://github.com/reduxjs/react-redux/tree/connect-subscription-experiments

A few notes:

  • We can't reuse the same subscription init approach we did in v5, because that relied on having access to this.context.store in the constructor. With createContext, there's no way to grab the store reference that early, because you only have access to it in render(). The only options there are to have a wrapper component whose only job is to extract whatever you need from context and pass it to a child component as a prop that does the real work, or use static contextType. I don't think either of those are workable here, because a wrapper adds more levels of nesting (probable perf hit) and v6 allows the user to customize what context instance they use at runtime (so we can't declare/assign that statically).
  • So, just for kicks, I've opted to jump right into experimenting with using hooks directly inside of connect, and writing direct subscription handling logic. I briefly considered trying to write a useRedux() hook first, and then use that in connect, but decided it was better to get connect working right first. We can extract that into a working hook later.
  • My first attempt passed the tests, but did all the derivation in the function component itself. A quick perf benchmark showed it was horribly slow, only 50% the speed of v5 (and slower than v6). Basically, I reinvented new context, badly.
  • My current WIP passes most tests, but the ones it doesn't pass are concerning. I think that batched updates are handling the "zombie child" issue, but in the case of children subscribing first, child props are being recalculated based on existing wrapperProps when the store updates, because the parent hasn't re-rendered itself yet and passed down new props. This makes me think we'd have to bring back something similar to the Subscription class we had in v5 to enforce top-down update behavior for that case.

FWIW, this is just me having hacked on stuff for a couple evenings - I'm just throwing out what I've tried and thought of so far.

Great work, I will try to dive deeper into it when I can, but I immediately had one question. Since connect is a HOC that returns a new component for each call, and custom context can only be configured in that outer function call, would it not be possible to use static contextType after all? Only way to change that context would be to call connect again to return a new component, so isn’t it static in that sense?

Haven’t checked your code or any details so might well be missing something which is why I’m asking. Might try to play with this tomorrow. :)

No, the v6 implementation specifically supports passing a custom context instance as a prop directly to a connected component, regardless of whether or not you also passed a custom context instance as an option when calling connect():

render() {
const ContextToUse =
this.props.context &&
this.props.context.Consumer &&
isContextConsumer(<this.props.context.Consumer />)
? this.props.context
: Context
return (
<ContextToUse.Consumer>
{this.indirectRenderWrappedComponent}
</ContextToUse.Consumer>

This was primarily intended to act as an alternative for the removal of the "store as prop" ability, so that you could have an isolated portion of the subtree reading from a different store than the rest of the app if desired.

Thanks for explaining, I had somehow missed that part. Certainly makes it trickier. :/

Using context from props does indeed seem like it necessarily adds another wrapper if not using hooks. I guess one approach to handle it with backwards compat might be to check if context-prop exists, not set up subscription in that case and render extra wrapper only then (+handle prop-change case..). Seems tricky and possibly verbose though.

Good performance in default case or custom context via connect, extra wrapper only if context is defined as prop? Most common case for that is tests as I understood it? Just brainstorming. :)

Could explain a little or point to a test of this "children first" scenario?

  • I think that batched updates are handling the "zombie child" issue, but in the case of children subscribing first, child props are being recalculated based on existing wrapperProps when the store updates, because the parent hasn't re-rendered itself yet and passed down new props.

I'd like to take a look at it.

I don't think we currently have tests that explicitly are supposed to check for that, but the tests that are failing on my branch right now seem to mostly represent that scenario.

Just to demonstrate what I meant by "wrap only if context exists in props" I refactored connectAdvanced in v6 to use static contextType, source can be found here. It passes all tests.

I choose v6 as basis because it was easier/quicker to demonstrate the approach there. A real solution would need to handle prop-changes as well. If it would be helpful I could give a shot to refactoring the 5.x-branch to use new context (and add support for context as prop) with this approach, which would have to be more complete since it has side-effects based on context.

This only addresses one of the problems you mentioned of course and it isn't necessarily a good solution even for that, but it is the only approach I can think of that avoids a breaking change and doesn't degrade performance for the common case.

Little bit re-familiarized myself with the code base again (haven't really kept up since 3.x) and having implemented unofficial redux hooks I kinda feel that it might not make sense to build connect() on top of the public redux hooks api. They kinda have different requirements. More on that later. It seems that it would make the most sense to implement a common Provider component for the connect() and the hooks api but otherwise have different code bases. Common provider would be required to be able to mix values from connect() and the hooks without having tearing issues.

Having played a bit my unofficial redux hooks api it's pretty clear that constraints that are present with the connect() are not with hooks. For examle with connect() you must provide everything in a single mapState call and even merge dispatch mapping in to the same result object. With hooks it's no problem to have multiple mapStates and mapDispatchs within the same component.

Example: https://github.com/epeli/typescript-redux-todoapp/blob/1062e1b444654add6ea409918b355a583fd73dd6/src/components/Main.tsx#L18-L20

For this reason I think the hooks api should not mimic the connect(). We can get away with a much simpler api which is more powerful. Creating that api should not be constrained by the connect() baggage.

Also ability to return a function from mapState for advanced scenarios is not required since you can just use the useMemo hook pretty easily. Here's a reselect example:

const selectUser = useMemo(
    () =>
        createSelector(
            state => state.users[props.userId],
            user => somethingExpensive(user),
        ),
    [props.userId],
);

const modifiedUser = useMapState(selectUser);

The deps array ensures that the cache is busted when required

I wrote here how some of these advanced scenarios could be handled with hooks

https://github.com/epeli/redux-hooks/blob/master/docs/optimizing.md

I find especially the useSelect hook interesting if we are interested in completely new APIs for react-redux.

Another interesting observation is that useMapState hook might get away with just a === check instead of doing shallow equal check because you can use it multiple times for different parts of the state you need. Shallow equal check is pretty much a workaround for the fact that connect() must always return a new object after all.

Also doing just the triple equals check would be closer to how the setState hook works.

Let's move the actual hooks API design discussion over to #1179 .

Well, good news and bad news.

The good news is that after hacking on stuff the last couple evenings, I managed to come up with a hooks-based connect implementation that uses direct store subscriptions, based on the Subscription class we had from v5.

The bad news: it ain't very fast.

+---------------------------------
¦ Version             ¦ Avg FPS ¦ 
+---------------------+---------+-
¦ 5.1.1               ¦ 37.91   ¦ 
+---------------------+---------+-
¦ 6.0.0               ¦ 28.23   ¦ 
+---------------------+---------+-
¦ 6.1.0-hooks-9c3bcb7 ¦ 23.86   ¦ 
+---------------------+---------+-
¦ 6.1.0-hooks-f8c506e ¦ 16.41   ¦ 
+---------------------------------

f8c506e was an earlier iteration that basically reinvented context. 9c3bcb7 is what I just completed.

With the usual caveats that these are artificial stress tests, it's pretty clear that these two attempts did not perform anywhere near as fast as v5 did. :(

Here's the very hacky implementation if anyone wants to look at it.

Awesome work, Mark! Can you post instructions on how to run the synthetic benchmarks?

I wish it were "awesome". It runs, but it's even slower than 6.0 is.

I just made a small tweak to use useLayoutEffect for the subscriptions instead of useEffect. May have made a tiny difference, but not enough to matter.

As for the benchmarks, see https://github.com/reduxjs/react-redux-benchmarks . You ought to be able to clone that, pull my branch, build React-Redux, drop the react-redux.min.js over in the benchmarks repo (with a distinct file name), and run things to replicate the approximate results.

Actual FPS values may vary, and I notice them varying from run to run myself. It's more about the relative results between the versions.

Soooo...

I may be on to something here.

I just made a very small change, and I got these results:

+---------------------------------
¦ Version             ¦ Avg FPS ¦ 
+---------------------+---------+-
¦ 5.1.1               ¦ 34.61   ¦ 
+---------------------+---------+-
¦ 6.0.0               ¦ 23.53   ¦ 
+---------------------+---------+-
¦ 6.1.0-hooks-84feb72 ¦ 19.69   ¦ 
+---------------------+---------+-
¦ 6.1.0-hooks-9c3bcb7 ¦ 20.22   ¦ 
+---------------------+---------+-
¦ 6.1.0-hooks-f8c506e ¦ 15.58   ¦ 
+---------------------+---------+-
¦ 6.1.0-hooks-memo    ¦ 38.12   ¦ 
+---------------------------------

Notice the results for 5.1.1 vs 6.1.0-hooks-memo :)

I only changed one line between 84f3b72 and that build. What was it?

-const Connect =  ConnectFunction;
+const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction;

That seems to have made a huge impact, and I'm seeing the same results consistently as I re-run the benchmarks. This latest commit is actually faster than 5.1!

inb4 someone screams "MEMO ALL THE COMPONENTS" and Dan shows up to tell them that's a bad idea

I just pushed that change up as 5cddd88 if anyone else wants to try it out.

SHIP IT

More seriously, this would require a v7 release... and probably more testing 🤔

Yeah, as I said at the start of the issue, any internal changes that require a bump to the React peer dependency would be a major version change for React-Redux, as that's a breaking change.

In theory, it'd still be nice to come up with at least one alternative implementation that fits the listed constraints, including "works on 16.4 and thus could be shipped as 6.1.0". In reality? I'm not entirely sure there's a feasible approach. Just needing to access the store via context in the constructor for a class version of connect would probably be an issue.

Poked a couple more things. I realized that the top-level connected components were still subscribing directly to the store, so their updates weren't actually being collected into unstable_batchedUpdates(). I tried modifying <Provider> to use a Subscriber instance inside, so that those top-level components should batch updates together.

I'm not seeing a particularly noticeable difference with the previous build, but it's certainly no worse, and both builds are still meaningfully faster than v6 and v5.

+-----------------------------------------
¦ Version                     ¦ Avg FPS ¦ 
+-----------------------------+---------+-
¦ 5.1.1                       ¦ 33.77   ¦ 
+-----------------------------+---------+-
¦ 6.0.0                       ¦ 26.52   ¦ 
+-----------------------------+---------+-
¦ 6.1.0-hooks-batchedprovider ¦ 35.67   ¦ 
+-----------------------------+---------+-
¦ 6.1.0-hooks-memo            ¦ 35.09   ¦ 
+-----------------------------------------

Pushed that change up too.

@markerikson Awesome work!

Regarding to your Twitter question I would really like to see major release asap so we can start experimenting with hook apis in real projects.

I would be very eager to try to port my hooks implementation to use the official Provider so I could put them into existing project using connect() to see how they work in real life code bases.

Yeah, as I said at the start of the issue, any internal changes that require a bump to the React peer dependency would be a major version change for React-Redux, as that's a breaking change.

In theory, it'd still be nice to come up with at least one alternative implementation that fits the listed constraints, including "works on 16.4 and thus could be shipped as 6.1.0". In reality? I'm not entirely sure there's a feasible approach. Just needing to access the store via context in the constructor for a class version of connect would probably be an issue.

@markerikson it is possible to check if the version of react is at least 16.8 and then use a hooks implementation? that way you can keep the react peer dependency version and users can gradually use the newer implementation when they upgrade to a newer version of react. ideally it would also tree shake the implementation that it's not using too

this is just a thought though. I haven't assessed if it's possible or makes sense

Not really, no.

I'm not going to ship duplicate implementations of connect in the same release. Not good for maintainability or bundle size.

v5 still works with every version of React >= 0.14, albeit with warnings in <StrictMode>. v6 works with >= 16.4, it's just not as ideal an implementation as we'd hoped. If staying on a specific non-latest version of React is a concern, those are both valid options.

Really, if you're on any React 16.x release, you ought to be able to upgrade to 16.8 without any breakages. The only tiny change that might be an issue was a specific tweak in behavior to getDerivedStateFromProps that was introduced in 16.3 and then changed in 16.4, and that was considered a bugfix. Other than that, if you're on 16.x, I don't see any reason why you shouldn't be able to bump to 16.8.

So, as I said earlier: I'd certainly like to come up with an implementation that only requires React 16.4 and also fits all the other constraints (better perf, direct subscriptions, no <StrictMode> warnings, etc), but at this point I think it's unlikely.

FWIW, I put up a Twitter poll asking how people felt about us possibly bumping our major version to v7 if it meant potentially faster perf and the ability to ship a public hooks API, and 90% are in favor of that:

https://twitter.com/acemarke/status/1093759113241100288

Version 6 seems mostly like a no "need to move to" if you can avoid using - and just stay on 5.
If you want to upgrade to react 16.8, you can use v7 and that is super fast and awesome.

  • React >= 0.14 you can use v5
  • React >= 16.8 you can use v7

This seems more than acceptable. (If v7 is faster than v5 that would be an additional reason to bump react, etc)

I have seen a number of issues in the React repo about weird / bad things happening when you mix legacy context and new context in the same React tree, and v5 still uses legacy context. Also, most of the React-Redux ecosystem has updated itself to work correctly with v6 (Redux-Form, React-Redux-Firebase, etc). So, there are plenty of valid reasons to use v6 right now.

@markerikson Yes, there are valid reasons to use it...but if you can avoid it (dont upgrade Redux-Form, etc) - seems somewhat valuable to wait for these issues to be resolved. I am unaware about mixing - etc - and did not mean to imply all your v6 work was completely 'invalid', etc. Appreciate you and this project!

The new version absolutely killed the performance of my relatively simple app, so I had to downgrade.

The use case is storing form values in Redux state (via redux-form). Handling of a keypress event takes 150-200ms on v6.0.0, as opposed to 25-40ms on v5.1.1. I am under the impression that such a big regression is atypical and while I can't provide the source code, I'd love to give any information necessary to construct a test case.

I have a pretty deep component tree because I use HOCs liberally, perhaps that's a factor?

@ancestorak : it would help if you can provide a link to a specific project or repo that demonstrates the issue, so that we can both see what's happening, and set up benchmarks that replicate that for later comparisons.

@markerikson Unfortunately the project is proprietary so I'll have to reduce it to a minimal test case. It would help to know what are the key factors to look out for. I'm not familiar with the internals of context update propagation, but I'm guessing tree size is important? Anything else?

I dunno what the key factors are - that's why I need to see a project that demonstrates this behavior :)

Day job's had me busy and mentally occupied the last couple weeks, but I've been able to make a bit of improvement to the benchmarks suite the last couple days. Want to give an update on where things stand.

I just merged in a benchmarks PR that bumps React to 16.8, captures initial mount and overall average render timings, and adds a new "deep tree" benchmark scenario.

In general, what I'm seeing is that the current experimental hooks-based implementation of connect appears to be very competitive with v5 in terms of update performance. It does seem to be a little bit slower than both v5 and v6 in initial mount time for the entire tree, but I'm not sure how much of an issue that is.

The actual specific numbers for FPS, mount time, and average update time fluctuate from run to run, but the behaviors are fairly consistent from run to run relative to each other.

Here's a sample run of the suite as an example. 6.1.0-hooks-batchedprovider is the last commit I pushed a few days ago, and 6.1.0-hooks-useeffect is a tweak I just made to switch from useLayoutEffect to useEffect internally just to see if there's a difference. (And yes, v6 truly does appear to be drastically slower in the "deeptree" benchmark - that result is consistent across runs.)

Results for benchmark deeptree:
+-------------------------------------------------------
¦ Version                     ¦ Avg FPS ¦ Render       ¦
¦                             ¦         ¦ (Mount, Avg) ¦
+-----------------------------+---------+--------------+
¦ 5.1.1                       ¦ 58.23   ¦ 123.6, 0.1   ¦
+-----------------------------+---------+--------------+
¦ 6.0.0                       ¦ 9.09    ¦ 119.9, 7.4   ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-batchedprovider ¦ 58.04   ¦ 125.1, 0.2   ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-useeffect       ¦ 58.50   ¦ 120.1, 0.1   ¦
+-------------------------------------------------------


Results for benchmark stockticker:
+-------------------------------------------------------
¦ Version                     ¦ Avg FPS ¦ Render       ¦
¦                             ¦         ¦ (Mount, Avg) ¦
+-----------------------------+---------+--------------+
¦ 5.1.1                       ¦ 33.14   ¦ 221.1, 0.6   ¦
+-----------------------------+---------+--------------+
¦ 6.0.0                       ¦ 12.28   ¦ 230.1, 8.6   ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-batchedprovider ¦ 31.88   ¦ 277.2, 0.4   ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-useeffect       ¦ 34.56   ¦ 292.8, 0.7   ¦
+-------------------------------------------------------


Results for benchmark tree-view:
+-------------------------------------------------------
¦ Version                     ¦ Avg FPS ¦ Render       ¦
¦                             ¦         ¦ (Mount, Avg) ¦
+-----------------------------+---------+--------------+
¦ 5.1.1                       ¦ 41.80   ¦ 602.6, 0.3   ¦
+-----------------------------+---------+--------------+
¦ 6.0.0                       ¦ 28.57   ¦ 581.6, 22.9  ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-batchedprovider ¦ 41.86   ¦ 692.4, 0.3   ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-useeffect       ¦ 41.56   ¦ 657.0, 0.3   ¦
+-------------------------------------------------------


Results for benchmark twitter-lite:
+-------------------------------------------------------
¦ Version                     ¦ Avg FPS ¦ Render       ¦
¦                             ¦         ¦ (Mount, Avg) ¦
+-----------------------------+---------+--------------+
¦ 5.1.1                       ¦ 45.50   ¦ 3.5, 0.5     ¦
+-----------------------------+---------+--------------+
¦ 6.0.0                       ¦ 28.28   ¦ 3.4, 4.9     ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-batchedprovider ¦ 48.82   ¦ 3.6, 0.2     ¦
+-----------------------------+---------+--------------+
¦ 6.1.0-hooks-useeffect       ¦ 48.31   ¦ 4.1, 0.4     ¦
+-------------------------------------------------------

I may try to modify the benchmark harness to run each benchmark+version several times, then average those to smooth out the fluctuations in individual runs.

I'd really appreciate some help working on that and trying to come up with some additional benchmark scenarios :)

I'd also still love to see other people attempt to reimplement the guts of connect themselves, or critique my current experimental implementation. It's entirely likely there's a better approach out there, or that I've done something sub-optimal with my implementation. More eyes on the code and more implementations to compare against would be great.

Awesome work Mark, benchmarks are looking great I'd say. :) I'm not that worried about the slight increase in mount times over 5.1.1 either.

I tried getting the branch connect-subscription-experiments to run to start experimenting with adding SSR-tests (more fun experimenting in an experimental branch 😉), but seems like src/utils/Subscription.js is missing?

@Ephem : Yipes, I'm a doofus :) Completely forgot to commit Subscription.js.

Just committed and pushed that.

And just took the time to clean up the branch. Removed dead code, fixed lint issues, formatted, and bumped test dep versions, and the branch now actually shows green on Travis.

@markerikson Not a doofus at all, that happens all the time when experimenting and moving fast. :)

Thanks for the cleanup as well, looks great! I got my first server-test passing and will continue experimenting.

I did notice one minor discrepancy though, install-test-deps.js is pointing at React 16.8, but run-tests.js is pointing at 16.6, so might want to bump that as well. :) Again, great work! 👏

These numbers look great @markerikson. Are you planning on moving it to a release?

@alexreardon : I'd still like to try to flesh out the benchmarking and test cases some more first. But, as I said, this is really where we need some community help to come up with appropriate test cases and benchmarks. For example, we don't currently have any tests that mimic an SSR or dynamically-loaded-reducers scenario, and that's not something I'm familiar with myself. I'd also really like to see some benchmarks that do things like "typing in a form".

To be blunt, despite my repeated requests for the community to help out with this effort overall, thus far it's pretty much been a one-Mark show.

@timdorr : we could potentially put up an alpha/preview build from this branch. Call it 7.0.0-alpha.0 or something, and bump the React peer dep to 16.8 .

I could try and give it a whirl if you do an alpha release or similar

@alexreardon : you should be able to pull the branch from https://github.com/reduxjs/react-redux/tree/connect-subscription-experiments and build it yourself if you'd like to try it out right now.

Okay sure. I will try to lock off some time tomorrow to get you some more data from react-beautiful-dnd

I will look at performance and behaviour

Thank you for the incredibly details post @markerikson. I really appreciate having all this insight into how react-redux is moving forward, and where the project needs support!

Greatly appreciate all the effort @markerikson. The in-depth explanation, helped me understand how it works internally much better. If you need someone to test anything on a larger applications just hit me up on twitter. I have several large codebases that use Redux and I wouldn't mind refactoring big parts to useRedux.

Awright, having heard the pleas of the masses, and following the advice of the ever-wise @timdorr:

I have published an alpha build as a temporary fork under the name @acemarke/react-redux.

The alpha build is available as @acemarke/react-redux@next, currently at version 7.0.0-alpha.1.

I suspect that switching over to this would cause some peer dep warnings, but at least it's something you can install off of NPM.

Please try it out and let us know how it behaves in comparison to v5 and v6 in real-world apps!

note: thanks to @Jessidhia for pointing out that you can do:

yarn add react-redux@npm:@acemarke/react-redux@next

which should work okay to skip the peer dep warnings.

Unofficial Release Notes

  • This does require a minimum React peer dep version of ^16.8.2, because hooks are a requirement for the implementation.
  • There's currently a hardcoded dependency on react-dom so we can use unstable_batchedUpdates. So, this won't work with React Native right now. Supposedly we can work around this by having some kind of a "platform.js" file that handles importing stuff from RN instead.

Didn't do much perf testing with this since my current project isn't very demanding, but at least I can confirm that everything is working as it should and nothing broke.

Does anyone have an example using the alpha branch that they could share?

@gretzky Usage is same as v6, nothing changed.

Thanks @saboya, let me rephrase-- a full example of redux/react-redux with hooks

I don't think this version contains hooks to use in place of connect just yet, but I might be wrong.

Correct. There's no new public APIs, hooks or otherwise. It's just a rewriting of the internals of connect() to use hooks to implement the wrapper component.

In theory, you'd just run yarn add react-redux@npm:@acemarke/react-redux@next and rebuild, and everything magically Just Works (TM).

Ok, I had some issues and had to rollback to v5. It seems to me that either components are not being updated in the correct order (parent-first) or connect is bailing out of running mapStateToProps because it can't pick up ownProps being updated.

I'm gonna try to put together a minimal reproducible test case, but to be honest I'm not really sure of what's going on. What I can tell you is that I'm updating a state using useEffect hook and passing it down. The first component picks it up, the second one doesn't.

It's something like this:

// props.arr is an immutable array that is updated
const component1 = (props) => {
  const [state, setState] = useState({ prop1: 'blabla', arr: props.arr })
  useEffect(() => setState({ ...state, arr: props.arr }), [props.arr])

  return <Component2 arr={state.arr} />
}

const Component2 = (props) => <div>
  <Component3 arr={props.arr} />
</div>

// mapStateToProps doesn't get called when ownProps.arr changes
const mapStateToProps = (state, ownProps) => ({
  whatever: arr.map(id => state.whatever.byId[id])
})

const TempComponent3 = <div>
   { ...do whatever with props.whatever }
</div>

const Component3 = connect(mapStateToProps)(TempComponent3)

@saboya : yeah, if you can put together a test that works in v5 / v6 and not in that v7 alpha, it would really help.

@einarq : preferences:

  • A CodeSandbox that demonstrates the issue with manual interaction
  • A cloneable CRA app
  • Something cloneable that shows the behavior automatically in unit tests somehow

Ok, I managed reduce it to a pretty basic application. I also managed to write a test in react-redux's code base that does pass in v5 / v6 but fails on v7, and opened a PR for it (don't expect it to be accepted as is, but it's a start):

#1195

The application that reproduces the issue can be found here:
https://github.com/saboya/react-redux-hooks-test-case

To make it easier to test, I added v5, v6 and v7-alpha versions as aliases and you can change them in webpack.config.js by switching the alias field for react-redux.

The application will print to console.log whenever it passes certain points, it's pretty self-explanatory. Here are the outputs for the 3 react-redux versions:

// react-redux-v5
COMPONENT 1 PROPS:  {list: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(0)}
COMPONENT 3 OWNPROPS:  {list: Array(0)}
COMPONENT 3 RENDER:  {list: Array(0), mappedProp: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(0)}
COMPONENT 1 PROPS:  {list: Array(1), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(1)}
COMPONENT 3 OWNPROPS:  {list: Array(0)}
COMPONENT 3 RENDER:  {list: Array(0), mappedProp: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(1)}
COMPONENT 3 OWNPROPS:  {list: Array(1)}
COMPONENT 3 RENDER:  {list: Array(1), mappedProp: Array(1), dispatch: ƒ}

// react-redux-v6
COMPONENT 1 PROPS:  {list: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(0)}
COMPONENT 3 OWNPROPS:  {list: Array(0)}
COMPONENT 3 RENDER:  {list: Array(0), mappedProp: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(0)}
COMPONENT 1 PROPS:  {list: Array(1), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(1)}
COMPONENT 3 OWNPROPS:  {list: Array(0)}
COMPONENT 3 RENDER:  {list: Array(0), mappedProp: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(1)}
COMPONENT 3 OWNPROPS:  {list: Array(1)}
COMPONENT 3 RENDER:  {list: Array(1), mappedProp: Array(1), dispatch: ƒ}

// react-redux-v7
COMPONENT 1 PROPS:  {list: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(0)}
COMPONENT 3 OWNPROPS:  {list: Array(0)}
COMPONENT 3 RENDER:  {list: Array(0), mappedProp: Array(0), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(0)}
COMPONENT 1 PROPS:  {list: Array(1), dispatch: ƒ}
COMPONENT 2 RENDER:  {list: Array(1)}
COMPONENT 3 OWNPROPS:  {list: Array(0)}
COMPONENT 2 RENDER:  {list: Array(1)}
COMPONENT 3 RENDER:  {list: Array(0), mappedProp: Array(0), dispatch: ƒ}

I might debug this myself later, but for now this might be of some help for you, @markerikson

@saboya error => useEffect(() => setState({ ...state, arr: props.arr }), [props.arr])

useEffect may be deferred, so you shoud use setState (prevState => ({ ...prevState , arr: props.arr }), [props.arr])

otherwise use useLayoutEffect that is equivalent of componentDidUpdate executed after every render.

Okay @markerikson, I have some good news for you

  • Testing on: react-beautiful-dnd
  • React: react@16.8.3
  • Machine: a beastly macbook pro (so numbers will be relative)
  • 501 connected components
  • Sample time based on an average of 200 updates

In development mode

process.env.NODE_ENV === 'development'

react-redux@5.0.7
Average time for move: ~6ms

acemarke/react-redux@next
Average time for move: ~5.5ms 👍

In production mode

process.env.NODE_ENV === 'production'

react-redux@5.0.7
Average time for move: ~0.8ms

acemarke/react-redux@next
Average time for move: ~1ms

I would not read too much into the loss of ~0.2ms. It looks like within the margin of error. Based on these findings I would strongly recommend moving to the new version

@alexreardon : Huh. Interesting that it's a bit slower in prod that way. But, good that it's competitive overall.

(also, are those numbers in seconds, or milliseconds?)

Do you think you could come up with some kind of a scenario for the benchmarks repo that would demonstrate this automatically?

Overall, the next step is probably to figure out how to set up an import redirect so that we can import unstable_batchedUpdates from React Native when used in that platform, and then wrap that up with some kind of a new batch() API we can export publicly. There was an earlier comment that linked to an example of this platform handling.

I've never used RN myself, so this is where I could really use some help from folks who have.

I'd also still really like to see some community contributions to help us mimic SSR and hot-loading scenarios, since we don't have any right now. I'll poke @theKashey for RHL, and @mpeyper / @Ephem / @abettadapur specifically with a request for some tests that would mimic SSR scenarios. (Others welcome to contribute too, I just know they've been involved in related issues.)

Also, as an FYI: I'm trying to see if I can pull together a class-based implementation that uses direct subscriptions again, but only requires React 16.4, so that we would be able to ship it as a v6.1 minor instead of a v7.0 major. I've got an early proof of concept that at least passes the tests for connect, but not yet connectAdvanced. I also have not benchmarked it at all. To be honest, I fully expect that it will be significantly slower than v7, because of how the internals are working out, but I want to have it for comparison purposes. I hope to make further progress on that over the weekend.

All those numbers I posted were milliseconds. I'll update the language

@alexreardon great!

could you update the same bench with react-redux 6.0.1 ?

@markerikson Thanks for the poke! I've been thinking about SSR and dynamic injection a bit and been poking around but haven't had time to do any "real" work, I will try to squeeze it in this weekend. Right now my thinking goes like this:

SSR - First step is some basic integration tests in a new suite just as a basic sanity check. A lot of tests I've tried to sketch out ends up testing more of implementation details or React though so will need to find a good balance.. Last time I got a bit paralysed trying to find that balance so now I'll try to just get a PR up with a bunch of tests testing current behaviour (documented or not) and we can use that to discuss which tests actually add value and remove others. :)

Dynamic injection of reducers - I think the best we can do here currently is to add tests for the different strategies the largest current approaches use (kind of like contract tests I guess). This is brittle since it depends on private APIs, but that way at least we are aware if something breaks current implementations. :)

@Ephem : great, thanks! Yeah, any tests at all would provide some benefit over our current complete lack of related tests. Even if the tests border on being "snapshot"-like, in that they only tell us if something has changed in some way, that would still be beneficial.

(To be honest, a lot of our current tests are actually kind of like that. Many of our tests try to specifically count the number of mapState calls that have occurred during the test, and that has varied noticeably over time as the implementation details have changed. So, a test might assert expect(mapStateSpy).toHaveBeenCalledTimes(3), and that test passes under vX, but fails under vY. A lot of my work in the v6/v7 dev process has been examining those tests and trying to reason through how the execution sequence behaved in vX, and what it's doing now in vY, and decide if the number passed to that assertion should change accordingly to make the test pass based on the current implementation logic.)

@markerikson @solkimicreb says ' unstable_batchedUpdates which seems to order re-renders in a parent-child order even if the setState calls are in a different order'. If that is true, v7 can drop create new Subscription in connectAdvanced ?

@wangtao0101 : nope.

First issue is that we have to actually have a place where we do wrap subscription callbacks in unstable_batchedUpdates().

Second issue is, while the re-renders within a batch will indeed run top-to-bottom, we need to run each component's mapState function with the latest fresh props from its parent. But, we also want to avoid re-rendering the wrapped component unless the derived props have actually changed. That means that we need the fresh props at the time we run mapState, but that has to be before we trigger the re-render.

v5 (and now v7-alpha) do this by using that Subscription class to trigger cascading updates. As the top level of connected components finish re-rendering, they notify the next layer of connected descendants that it's safe to try to update themselves.

As I've said several times, if anyone can come up with a better implementation that actually passes all the tests, I'm entirely open to seeing how it compares. But, so far, no one else has even taken a stab at it besides me.

@saboya , @einarq : thanks for the test and repro project. I'm pretty sure I figured out the issue. It's specifically the if statement in here:

const actualChildProps = usePureOnlyMemo(
() => {
if (childPropsFromStoreUpdate.current) {
return childPropsFromStoreUpdate.current
}
// TODO We're reading the store directly in render() here. Bad idea?
return childPropsSelector(store.getState(), wrapperProps)
},
[store, previousStateUpdateResult, wrapperProps]
)

The idea there is that the store subscriber callback puts the new derived child props in that ref if things changed, then forces a re-render.

But, if the wrapper props change after that re-render is queued, but before the wrapper component has re-rendered, then it's going to see that those derived child props from the store update exist and use them without re-calculating the derived child props using the latest wrapper props. And there's the bug.

The fix is to change it to:

if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}

I've just published that change as v7.0.0-alpha.3. Give it a shot and see if it works any better.

Execution Sequence Analysis

In order to make sure I understood what was going on, I took the time to type out the exact behavior sequences for the test app at https://github.com/saboya/react-redux-hooks-test-case , as best as I understand them, for v5 / v6 / v7 alpha.1 / v7 alpha.2 . Here's what I came up with (warning: Wall O' Text ahead):

Note: useEffect callbacks execute bottom-up, like cDM / cDU. Any
work queued in a useEffect callback is executed synchronously, after all
the callbacks have been executed. Reference: ReactFiberScheduler.js lines 574-581

v5

  1. Initial Render

    1. Connect(C1).constructor: C1 mapState
    2. Connect(C1) render
    3. C1 render
    4. Connect(C2).constructor: C2 mapState
    5. Connect(C2) render
    6. C2 render
    7. Render pass complete - React pauses before executing effects
    8. Effects execute:
      1. C1 useEffect: setState() queues update after effects
      2. Post-effects synchronous re-render pass
      3. C1 render
      4. Connect(C2).cWRP: re-runs selector, props shallow equal, no update
      5. Connect(C2) sCU: returns false
  2. Timeout -> dispatch UPDATE_STUFF

    1. store notifies subscribers, including Connect(C1)
    2. Connect(C1).handleChange: C1 mapState
    3. Connect(C1) calls setState
    4. Connect(C1) render
    5. C1 render
    6. Connect(C2).cWRP: C2 mapState
    7. Connect(C2).sCU: true
    8. Connect(C2): render
    9. C2: render
    10. Render pass complete - React pauses before executing effects
    11. Effects execute:
      1. C1 useEffect: setState() queues update after effects
      2. Post-effects synchronous re-render pass
      3. C1 render
      4. Connect(C2).cWRP: re-runs selector, props different, C2 mapState
      5. Connect(C2) sCU: true
      6. Connect(C2): render
      7. C2: render

v6

  1. Initial Render

    1. Connect(C1) constructed
    2. Connect(C1): render, C1 mapState
    3. C1 render
    4. Connect(C2) constructed
    5. Connect(C2): render, C2 mapState
    6. C2 render
    7. Render pass complete - React pauses before executing effects
    8. Effects execute:
      1. C1 useEffect: setState() queues update after effects
      2. Post-effects synchronous re-render pass
      3. C1 render
      4. Connect(C2).render: re-runs selector, props shallow equal, no update, returns same child element
  2. Timeout -> dispatch UPDATE_STUFF

    1. Store notifies subscribers - only <Provider>
    2. Provider calls setState({storeState})
    3. Synchronous render pass
      1. Connect(C1).render: context changed, re-runs selector, store state different, C1 mapState, new child element
      2. C1 render
      3. Connect(C2).render: context changed, re-runs selector, store state different, C2 mapState, new child element
      4. C2 render
      5. Render pass complete - React pauses before executing effects
    4. Effects execute:
      1. C1 useEffect: setState() queues update after effects
      2. Post-effects synchronous re-render pass
      3. C1 render
      4. Connect(C2).render: wrapperProps changed, re-runs selector, C2 mapState, new child element
      5. C2 render

v7 alpha

  1. Initial Render
    1. Connect(C1) renders: first run, memo fails, C1 mapState, new child element
    2. C1 render
    3. Connect(C2) renders: first run, memo fails, C2 mapState, new child element
    4. C2 render
    5. Render pass complete - React pauses before executing effects
    6. Effects execute, bottom-up:
      1. Connect(C2) useEffect: subscribes, checks store state, no change
      2. C1 useEffect: setState() queues update after effects
      3. Connect(C1) useEffect: subscribes, checks store state, no change, notifies descendants
      4. Connect(C2) subscriber: re-checks store state, props and store state shallow equal, no change
      5. Synchronous re-render pass
      6. C1 render
v7.alpha.1 behavior
  1. Timeout -> dispatch UPDATE_STUFF
    1. Store notifies subscribers
    2. Connect(C1) subscriber: store state changed, C1 mapState, sets childPropsFromStoreUpdate.current, triggers force update reducer
    3. Batched render pass
    4. Connect(C1) renders: store state changed, memo fails, has childPropsFromStoreUpdate.current, returns new child element
    5. C1 render
    6. Connect(C2) renders: wrapper props unchanged, memo succeeds, returns same child element
    7. Render pass complete - React pauses before executing effects
    8. Effects execute, bottom-up:
      1. C1 useEffect: setState() queues update after effects
      2. Connect(C1) useEffect: has childPropsFromStoreUpdate.current, notifies nested subs
      3. Connect(C2) subscriber: checks store state, C2 mapState, set childPropsFromStoreUpdate.current, triggers force update reducer
      4. Post-effects synchronous re-render pass
      5. C1 render
      6. Connect(C2) render: has childPropsFromStoreUpdate.current, returns new child element (but child props are from the store update and are stale - they don't include the new props from C1!!!)
      7. C2 render (with incorrect props)
v7.alpha.2 behavior
  1. Timeout -> dispatch UPDATE_STUFF
    1. Store notifies subscribers
    2. Connect(C1) subscriber: store state changed, C1 mapState, sets childPropsFromStoreUpdate.current, triggers force update reducer
    3. Batched render pass
    4. Connect(C1) renders: store state changed, memo fails, has childPropsFromStoreUpdate.current, returns new child element
    5. C1 render
    6. Connect(C2) renders: wrapper props unchanged, memo succeeds, returns same child element
    7. Render pass complete - React pauses before executing effects
    8. Effects execute, bottom-up:
      1. C1 useEffect: setState() queues update after effects
      2. Connect(C1) useEffect: has childPropsFromStoreUpdate.current, notifies nested subs
      3. Connect(C2) subscriber: checks store state, C2 mapState, set childPropsFromStoreUpdate.current, triggers force update reducer
      4. Post-effects synchronous re-render pass
      5. C1 render
      6. Connect(C2) render: has childPropsFromStoreUpdate.current, but ignores that because wrapperProps changed, re-runs C2 mapState with latest wrapperProps and latest storeState
      7. C2 render (with correct props)

Also, now my head hurts.

It occurred to me that all of the benchmarks currently only test cases where a single component (or a couple of its ancestors) need to update. We don't have any cases where a single store update results in many components needing to re-render.

I'd previously added a "deep tree" benchmark, where only the leaf components are connected. Originally, only a single leaf counter component at a time was being updated.

I just modified it so that many of the updates caused either 1/5 or 1/3 of the counters to increment themselves, to force many components to re-render at once.

The results were... surprising:

Results for benchmark deeptree:
+----------------------------------------+
¦ Version       ¦ Avg FPS ¦ Render       ¦
¦               ¦         ¦ (Mount, Avg) ¦
+---------------+---------+--------------¦
¦ 5.1.1         ¦ 13.28   ¦ 119.4, 0.1   ¦
+---------------+---------+--------------¦
¦ 6.0.0         ¦ 11.91   ¦ 117.6, 3.9   ¦
+---------------+---------+--------------¦
¦ 7.0.0.alpha-3 ¦ 23.56   ¦ 113.8, 2.0   ¦
+----------------------------------------+

Not only does v7 appear to be dead-on competitive in "single-component-update" scenarios... it appears to be vastly faster in "many-component-update" scenarios.

If you look at the "average time for a single render" value, you'll see something that seems counter-intuitive. If v5 is averaging 0.1ms for a single render, and v7 is averaging 2.0ms, how can v7 be running at almost twice the FPS of v5?

I'm pretty sure that it's because every single connected leaf component is becoming a tiny synchronous re-render in v5, but it's a separate synchronous render. On the other hand, because we're using unstable_batchedUpdates in v7, all those different leaf components are getting updated in a single render pass. So, v5's many 0.1ms renders are adding up to more time than v7's single 2.0ms render. (Meanwhile, v6 is also likely causing a single render per store update, but has to spend more time propagating data through context and walking the whole tree for each pass.)

So... it looks like v7 may actually be a perf improvement depending on how many components need to be updated at once.

update

@dai-shi pointed out that I actually sorta-kinda broke the deeptree benchmark when I made those edits. The reducers were handling the "multiple updates" cases, but doing nothing for the "single update" actions, so those were kind of becoming no-ops.

After fixing the logic and re-running the benchmark, the relative behavior is still the same. Updated numbers:

+------------------------------------------
¦ Version       ¦ Avg FPS ¦ Render       ¦ 
¦               ¦         ¦ (Mount, Avg) ¦ 
+---------------+---------+--------------+-
¦ 5.1.1         ¦ 12.64   ¦ 136.4, 0.1   ¦ 
+---------------+---------+--------------+-
¦ 6.0.0         ¦ 7.36    ¦ 132.5, 7.3   ¦ 
+---------------+---------+--------------+-
¦ 7.0.0.alpha-3 ¦ 20.04   ¦ 171.6, 1.1   ¦ 
+------------------------------------------

I'm actually confused on how the average render time dropped for v7, but went up for v6. v7 dropping makes sense - we now have about half of the dispatched actions resulting in single-component updates, so that's reasonable. Can't explain why v6's average render time is slower, though.

Anyway, main point still stands - v7 alpha appears to be the fastest one yet for the multi-update case, most likely thanks to the batching.

@markerikson I'm glad the test case helped you narrow down the problem, thanks for your great work. I'll be testing v7.0.0.alpha3 and I'll let you know if any new problems come up.

How feasible it is to add unstable_batchedUpdates to v5? That way you'd be able to do a comparison with both implementations using similar algorithms to compare benchmarks against.

@saboya : mmm... wouldn't be anything we'd actually publish officially, but might be worth doing a temp fork to see what happens.

Given that I've got that logic basically isolated in Subscription, it might be feasible to try it out without too much hassle.

I did some further poking at the notional "v6.1 with subscriptions, classes, and batched updates" branch in https://github.com/reduxjs/react-redux/tree/connect-class-subscription-experiments . Got the tests at least passing, and decided to run it through the benchmarks suite.

As I expected, results were pretty ugly:

Results for benchmark deeptree:
+-----------------------------------------
¦ Version       ¦ Avg FPS ¦ Render       ¦
¦               ¦         ¦ (Mount, Avg) ¦
+---------------+---------+--------------+
¦ 6.1.0-classes ¦ 2.68    ¦ 253.5, 14.1  ¦
+-----------------------------------------

Results for benchmark forms:
+-----------------------------------------
¦ Version       ¦ Avg FPS ¦ Render       ¦
¦               ¦         ¦ (Mount, Avg) ¦
+---------------+---------+--------------+
¦ 6.1.0-classes ¦ 24.25   ¦ 1619.0, 0.9  ¦
+-----------------------------------------

Results for benchmark stockticker:
+------------------------------------------
¦ Version       ¦ Avg FPS ¦ Render        ¦
¦               ¦         ¦ (Mount, Avg)  ¦
+---------------+---------+---------------+
¦ 6.1.0-classes ¦ NaN     ¦ 1745.9, 188.6 ¦
+------------------------------------------

Results for benchmark tree-view:
+-----------------------------------------
¦ Version       ¦ Avg FPS ¦ Render       ¦
¦               ¦         ¦ (Mount, Avg) ¦
+---------------+---------+--------------+
¦ 6.1.0-classes ¦ 53.27   ¦ 3059.8, 0.9  ¦
+-----------------------------------------

Results for benchmark twitter-lite:
+-----------------------------------------
¦ Version       ¦ Avg FPS ¦ Render       ¦
¦               ¦         ¦ (Mount, Avg) ¦
+---------------+---------+--------------+
¦ 6.1.0-classes ¦ 5.87    ¦ 7.7, 17.3    ¦
+-----------------------------------------

Now, it's entirely possible that I've just screwed up somewhere and I'm missing a key piece that will make this work much faster. I've only hacked on this for a couple evenings. But, these results are basically what I expected. Adding an additional wrapper component to handle pulling the store out of context, and all the other adjustments I had to make, seem to make this approach basically dead in the water.

At this point I'm really not seeing a reason to continue trying to work on this branch.

commented

@markerikson Thanks for these awesome updates, they're super interesting.

Sorry I don't fully follow, however - can you explain why you expected the results to be ugly? Reading through this thread, it sounded like the performance differences were mainly attributed to:

  • direct subscription vs using context API, and
  • batched updates

so a branch that brings back both these things, I would expect to perform better, no? Yet I see that the FPS for deeptree has dropped from 7.36 to 2.68. Thanks for clarifying.

@zxti: Sure. There's a variety of factors going into it:

  • In React 16, function components have less overhead than class components do, because there's no lifecycles to run and vvarious other checks can be skipped (reference: tweets by Andrew and Dominic ). The difference may not be much, but it would add up as the number of components increases.
  • Changes in how we use context:
    • connect has always used a single wrapper component around your own component. Up through v5, that wrapper was a class component using legacy context. React ensured that values read from the context were immediately available as this.context.whatever in the component's constructor. So, you can see the v5 implementation was able to access the store right away to initialize itself.
    • But, with new context, the API is based on render props. There's no way to read a value from context in the lifecycle methods of the component that renders the consumer. Originally, the only alternative is to create a wrapper component to render the context and pass the value to another component as props.
    • In React 16.6, the React team offered up another option: attaching a context instance to a component as static contextType = MyContextInstance; (docs reference).
    • But, we can't use static contextType for two reasons: v6 allows dynamically passing in a custom context instance as a prop, and use of contextType would require bumping our React peerDep from 16.4 to 16.6, which would also mean we'd have to do a major bump to v7... and the point of this experimental v6.1 branch was to see if there was a viable solution that didn't require a major version bump.
    • So, our only option here was to use a wrapper component just for context, but that meant inserting a second wrapper component on top of the original wrapper component we already had, just for the purposes of grabbing the store from context. Larger component tree, more overhead.
  • In order to process potential changes correctly, the main wrapper component (the one that subscribes to the store) had to implement getDerivedStateFromProps. That's another lifecycle method that's always getting called, and it actually meant that mapState was technically running more often than it used to.

All that adds up to a lot of extra overhead. Batching might make up for a bit of that, but it's going to be a net negative for perf. You might not notice it if you just click a button once to dispatch an action, but anything more involved would start to exhibit meaningful slowdowns.

So, just to be clear:

  • v7 alpha: direct subscriptions, batched updates, wrappers are function components + hooks
  • v6.1 experiment: direction subscriptions, batched updates, wrappers are class components (with an extra wrapper to read from context)

Hi guys,
i have done 2 little test with 5.1.1

  • 5.1.1-batched -> just add batched wrapper on notify()
  • 5.1.1-batched-newContext-static -> new Provider Class with new Context + Connect.contextType = ReactReduxContext

It seems that batched_updates increase Render Avg, but not with new Context .... is that strange?

Results for benchmark deeptree:
┌───────────────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬────────────────┐
│ Version                           │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values     │
│                                   │         │ (Mount, Avg) │           │           │          │                │
├───────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1                             │ 30.01   │ 67.7, 0.0    │ 2970.02   │ 1240.82   │ 517.90   │ 29,31,30,30    │
├───────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1-batched                     │ 59.00   │ 69.2, 1.0    │ 1490.85   │ 1889.24   │ 984.25   │ 59,59          │
├───────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1-batched-NewContext-static   │ 30.35   │ 67.7, 0.0    │ 2909.37   │ 1246.69   │ 520.06   │ 30,31,30,31,31 │
├───────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.0                             │ 26.68   │ 68.6, 2.0    │ 3011.92   │ 1183.44   │ 498.57   │ 26,27,27       │
├───────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.1-classes                     │ 25.00   │ 70.2, 1.5    │ 2918.88   │ 1200.10   │ 543.54   │ 25,25          │
├───────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 7.0.0.alpha-3                     │ 59.36   │ 69.6, 1.1    │ 1576.48   │ 1838.49   │ 982.35   │ 60,59,60,60    │
└───────────────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴────────────────┘

@salvoravida : can you push up your branches somewhere so I can look at them?

Also, did you modify any settings for running that benchmark? I'd expect there to be a lot more FPS value changes if they were running for the default 30 seconds.

@markerikson

here is a full 30sec test

Results for benchmark deeptree:
┌──────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬──────────────────────────────────────────────────────────────────────┐
│ Version                  │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                           │
│                          │         │ (Mount, Avg) │           │           │          │                                                                      │
├──────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 5.1.1                    │ 30.58   │ 68.5, 0.0    │ 17997.84  │ 7489.32   │ 2840.87  │ 29,30,31,32,31,30,31,30,31,30,31,32,30,29,31,30,31,30,32,30,31,30,30 │
├──────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 5.1.1-batched            │ 59.04   │ 69.7, 0.9    │ 8824.26   │ 11468.29  │ 5993.16  │ 60,59,60,59,60,59,60,58,60,59,60,59,60,54,58,60,57,60,59,60,59,59    │
├──────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 5.1.1-batched-NewContext │ 30.28   │ 66.4, 0.0    │ 17997.95  │ 7514.48   │ 2821.12  │ 30,31,30,27,30,31,30,32,30,31,30,32,31,30,31,31                      │
├──────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 6.0.0                    │ 26.11   │ 66.4, 2.0    │ 17911.66  │ 7259.25   │ 3034.80  │ 26,27,26,27,26,27,26,26                                              │
├──────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 6.0.1-classes            │ 30.00   │ 70.4, 1.4    │ 17452.67  │ 7369.85   │ 3275.61  │ 29,30,31,30,30                                                       │
├──────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 7.0.0.alpha-3            │ 59.21   │ 70.9, 1.0    │ 9480.06   │ 11198.21  │ 5763.04  │ 59,60,59,60,58,60,58,60,59,60,58,59,60,59,60,59,60,59,60,59,59       │
└──────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴──────────────────────────────────────────────────────────────────────┘

https://github.com/salvoravida/react-redux/commits/v5.1.1-batched-only
https://github.com/salvoravida/react-redux/commits/v5.1.1-batched-newContext-static

@salvoravida : thanks. I'll have to try poking at those at some point. tbh, skimming the commits, I'm not sure that the current changes are entirely valid, but I definitely appreciate you taking some time to play around with this yourself.

Provider with new Context is just cut&paste from "connect-class-subscription-v6" branch.
of course connectAdvance is just the old v5 with static Context.
i have tested it in a production App, and all seems to be fine.

the strange points are:

  1. why newContext get half Avg FPS even if with batched_updates, but take Render Avg better?
  2. it seems that newContext does double render as Avg FPS is just half ....

@markerikson fixed missed old [subscriptionKey] - sorry

Results for benchmark deeptree:
┌─────────────────────────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬────────────────┐
│ Version                                     │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values     │
│                                             │         │ (Mount, Avg) │           │           │          │                │
├─────────────────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1                                       │ 29.40   │ 70.3, 0.0    │ 2918.88   │ 1249.53   │ 526.90   │ 28,30,31,31    │
├─────────────────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1-batched                               │ 59.64   │ 67.2, 0.9    │ 1434.19   │ 1909.19   │ 1009.68  │ 60,59,60,59,59 │
├─────────────────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1-batched-NewContext-fixParentSubKey    │ 58.07   │ 67.2, 0.9    │ 1446.86   │ 1920.80   │ 1010.01  │ 56,59,60,60    │
├─────────────────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.0                                       │ 27.00   │ 67.2, 2.0    │ 2964.91   │ 1210.60   │ 510.67   │ 27,27          │
├─────────────────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.1-classes                               │ 29.00   │ 69.8, 1.5    │ 2836.00   │ 1265.17   │ 562.22   │ 29,29          │
├─────────────────────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 7.0.0.alpha-3                               │ 59.36   │ 70.7, 1.1    │ 1577.75   │ 1861.70   │ 970.53   │ 60,59,60,60    │
└─────────────────────────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴────────────────┘

so it seems that

  1. batched_updates get better FPS but increase single render
  2. NewContext (with static propagation) is irrelevant
  3. 5.1.1+batched_updates + newContext (static) has same performance of v7.0a3.
commented

@markerikson Ah, so to recap (please correct me if I'm off):

  • v5 passes down the store instance (and not the store state value) as context, using the legacy context API, and this context is pushed just once (since the store instance itself never changes). Each component then attaches itself to the store instance as a subscriber to listen for changes. This whole approach is called "direct subscription."
  • v6 passes down the store state value as context (using the new context API), meaning every time the store changes, we re-push context. Components no longer attach themselves as subscribers. React's context update implementation is slow, however, as it needs to walk the full tree (it can't iterate over only the components listening for that context).
  • v6.1 experiment reintroduces "direct subscription" by doing what v5 did but with the new context API—pass down the store instance (and not state values every time it changes), thus only pushing context once, and each component attaches itself to the store instance as a subscriber. So that much ought to be fast again....
    • But the other reasons you list—function components slightly faster than class components, two components per connect, and getDerivedStateFromProps—(much) more than negate the speedup?

@zxti : yep, that's a pretty good summary!

And v7-alpha reintroduces direct subscriptions, but does so using a function component for connect with hooks inside, instead of a class component.

Hi all,
some more experiments:

Results for benchmark deeptree:
┌──────────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬──────────────────────────────────────────────────────────────────────┐
│ Version                      │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                           │
│                              │         │ (Mount, Avg) │           │           │          │                                                                      │
├──────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 5.1.1                        │ 30.32   │ 69.3, 0.0    │ 17618.28  │ 7530.25   │ 3083.40  │ 31,32,31,32,30,31,22,30,31,29,28,30,29,32,31,30,31,32,31,29,31,31    │
├──────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 5.1.1-batched-NoWillReceive  │ 58.83   │ 69.6, 0.9    │ 8550.93   │ 11602.63  │ 5995.12  │ 60,59,60,59,60,59,60,59,60,59,60,59,58,59,60,58,59,58,51,51          │
├──────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 6.0.0                        │ 27.00   │ 67.2, 1.9    │ 17973.47  │ 7324.94   │ 2985.01  │ 27,27                                                                │
├──────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 6.0.1-classes                │ 30.46   │ 71.2, 1.4    │ 18233.43  │ 7355.95   │ 2723.99  │ 29,31,30,31,30,31,30,31,30,31,30,31,30,31,30,31,31                   │
├──────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 6.0.1-classes-NoGetDer-static│ 59.14   │ 68.6, 0.9    │ 8514.29   │ 11664.64  │ 6001.74  │ 59,60,59,60,59,60,59,60,59,60,59,60,59,60,58,60,58,60,56,59,58,58    │
├──────────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ 7.0.0.alpha-3                │ 58.78   │ 71.6, 1.0    │ 9497.43   │ 11261.51  │ 5651.67  │ 54,59,60,59,58,59,60,59,60,59,60,59,60,58,60,59,58,60,52,60,59,60,60 │
└──────────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴──────────────────────────────────────────────────────────────────────┘

wait just a few tests ...

great news :

┌───────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬────────────────┐
│ Version       │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values     │
│               │         │ (Mount, Avg) │           │           │          │                │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1         │ 29.72   │ 69.3, 0.0    │ 2926.76   │ 1231.81   │ 516.42   │ 29,30,31,31    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.2.1         │ 59.36   │ 68.9, 1.0    │ 1487.73   │ 1902.74   │ 970.93   │ 60,59,60,60    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.0         │ 28.04   │ 66.8, 2.1    │ 3065.45   │ 1200.56   │ 436.10   │ 27,29,28,29,29 │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.1-classes │ 26.00   │ 71.2, 1.5    │ 2989.46   │ 1135.38   │ 522.30   │ 26,26          │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.2.1         │ 59.36   │ 68.6, 1.0    │ 1429.56   │ 1906.84   │ 989.53   │ 60,59,60,60    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 7.0.0.alpha-3 │ 59.68   │ 73.1, 1.1    │ 1606.08   │ 1863.47   │ 945.96   │ 60,59,60,60    │
└───────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴────────────────┘

Test Suites: 5 passed, 5 total
Tests: 2 skipped, 76 passed, 78 total
Snapshots: 0 total
Time: 2.934s
Ran all test suites.

let's clean the code, make PRs ....

5.1.1 to 5.2.1 - remove deprecated lyfecycle - batchedUpdates
#1200

Abstract

  • Remove deprecate componentWillReceiveProps

First of all we should have a look to React Internals:

// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(current, workInProgress, ctor, newProps, renderExpirationTime) {
  var instance = workInProgress.stateNode;

 //...................

  // In order to support react-lifecycles-compat polyfilled components,
  // Unsafe lifecycles should not be invoked for components using the new APIs.
  if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function')) {
    if (oldProps !== newProps || oldContext !== nextContext) {
      callComponentWillReceiveProps(workInProgress, instance, newProps, nextContext);
    }
  }

 //...................

  var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);

  /// ......

  return shouldUpdate;
}

so we can replace

componentWillReceiveProps(nextProps){
   doSomeCalculation(nextProps)
}

with

shouldComponentUpdate(nextProps) {
     let update false;
        if (nextProps !== this.props)  {
            update = doSomeCalculation(nextProps)
         }
        return update
      }

that for react-redux use case (calculate new mapStateToProps when ownProps may have changed),
is safe. (we do not have to call this.setState into doSomeCalculation

  • add batched updates to React-Router 5.x

even if in 6.x we have already batched updates, i think it is right to upgrade 5.x
due to compatibilty to React 15 that has not new Context.

with this PR 5.x can double performance on deep-tree bench

Results for benchmark deeptree:

┌───────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬────────────────┐
│ Version       │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values     │
│               │         │ (Mount, Avg) │           │           │          │                │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1         │ 29.72   │ 69.3, 0.0    │ 2926.76   │ 1231.81   │ 516.42   │ 29,30,31,31    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.2.1         │ 59.36   │ 68.9, 1.0    │ 1487.73   │ 1902.74   │ 970.93   │ 60,59,60,60    │
├───────────────┼─────────┼──────────────┼───────────┼───────────

Test Suites: 5 passed, 5 total

if you want to test this PR right now you can use Yarn alias npm

   "react-redux": "npm:@salvoravida/react-redux@^5.2.1"

Your feedback is highly appreciated .... :D

6.0.1 to 6.2.1 - refactor - fix Performance - static contextType #1201
#1201

Abstract

This PR should fix performance issue with new 6.x, and restore it to 5.x speed

  • Remove getDerivatedStateFromProps

First of all we should have a look to React Internals:

// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(current, workInProgress, ctor, newProps, renderExpirationTime) {
  var instance = workInProgress.stateNode;

 //...................

  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
    newState = workInProgress.memoizedState;
  }

  var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);

  /// ......

  return shouldUpdate;
}

so we can replace

static getDerivedStateFromProps (nextProps){
   doSomeCalculationAndMemoizeToFakeState(nextProps)
}

with

shouldComponentUpdate(nextProps) {
     let update false;
        if (nextProps !== this.props)  {
            update = doSomeCalculation(nextProps)
         }
        return update
      }

that for react-redux use case (calculate new mapStateToProps when ownProps may have changed),
is safe.

  • use static contestType propagation => no double wrapper

With this pr i have removed double wrapper for context propagation and i use only static class contextType.

Constraints :
Custom context can be assigned only on connect options

connect(mapState,mapDispat, { context:CustomContext })(Component)

tbh i think that context on props should be removed as it is a duplication of connect options.

Anyway there are some alternative 💯
1)Just use a different ConnectedComponet for every context you need, like:

const MyConnectedComponentWithContext1= connect(mapState,mapDispat, { context:CustomContext1 })(Component)
const MyConnectedComponentWithContext2= connect(mapState,mapDispat, { context:CustomContext2 })(Component)
  1. use read_context from ReactInternals to allow use context as props on
<ConnectedComponent context={CustomContext} />

you can activate it with unstable_enableReadContextFromProps = true on connect options

const ConnectedComponent = connect(mapState,mapDisp, { unstable_enableReadContextFromProps:true })(Component)

I already use it successfully on https://github.com/salvoravida/react-universal-hooks
for useClassContext (useContext for classes) here https://github.com/salvoravida/react-class-hooks/blob/master/src/core/useClassContext.js

Results for benchmark deeptree:
┌───────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬────────────────┐
│ Version       │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values     │
│               │         │ (Mount, Avg) │           │           │          │                │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.1.1         │ 29.72   │ 69.3, 0.0    │ 2926.76   │ 1231.81   │ 516.42   │ 29,30,31,31    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 5.2.1         │ 59.36   │ 68.9, 1.0    │ 1487.73   │ 1902.74   │ 970.93   │ 60,59,60,60    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.0         │ 28.04   │ 66.8, 2.1    │ 3065.45   │ 1200.56   │ 436.10   │ 27,29,28,29,29 │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.0.1-classes │ 26.00   │ 71.2, 1.5    │ 2989.46   │ 1135.38   │ 522.30   │ 26,26          │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 6.2.1         │ 59.36   │ 68.6, 1.0    │ 1429.56   │ 1906.84   │ 989.53   │ 60,59,60,60    │
├───────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────┤
│ 7.0.0.alpha-3 │ 59.68   │ 73.1, 1.1    │ 1606.08   │ 1863.47   │ 945.96   │ 60,59,60,60    │
└───────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴────────────────┘

Test Suites: 5 passed, 5 total

  • React peer version >16.6.0 (static contextType)

if you want to test this PR right now you can use Yarn alias npm

   "react-redux": "npm:@salvoravida/react-redux@^6.2.1"

Your feedback is highly appreciated .... :D

Testing "react-redux": "npm:@salvoravida/react-redux@^6.2.1" right now in a medium complex application.

Seems fine and with some performance boost. Thanks!

@salvoravida We can't bump the peer dep without releasing as a new major. We would be breaking our API for users of React 16.4 or 16.5.

Is it possible to conditionally use staticContext if available? Not the prettiest solution, but at least some people can benefit from it.

Is it possible to conditionally use staticContext if available? Not the prettiest solution, but at least some people can benefit from it.

it is already so. You can use customContext in 6.0.1 and in my 6.2.1.

But let's discuss about customContext and React-Redux

99% You do NOT need customContext on React-Redux

In 99% of Apps you have One Store @ App Root.

If this is your case just use 6.x the SAME EXACT WAY you have already used 5.x

<Provider store={store}/>
 <App>
  <ConnectedComp />
</App>
</Provider >
//where
const ConnectedComp = connect(mapState,mapDisp)(Component);

In this case you can think to 6.x as Exact 5.x with deprecated Warning removed (deprecated lyfecicles and legacyContext) on React 16 so it is React 17 Ready.

1% You May need customContext on React-Redux

In 1% of Apps you have to use Two or More Store on the same app.

If this is your case, in both 6.0.0 and 6.2.1 you can have this code

<Provider store={store} context={CustomContext1}/>
 <App>
      <Provider store={store2} context={CustomContext2}/>
          <ConnectedComponentWithContext1/>
          <ConnectedComponentWithContext2/>
       </Provider > 
 </App>
</Provider >
//where
const ConnectedComponentWithContext1= connect(mapState,mapDispat, { context:CustomContext1 })(Component)
const ConnectedComponentWithContext2= connect(mapState,mapDispat, { context:CustomContext2 })(Component)

Also in 6.0.0 you can write an EQUIVALENT way that is no more supported (for performance issue) on 6.2.1

<Provider store={store} context={CustomContext1}/>
 <App>
      <Provider store={store} context={CustomContext2}/>
          <ConnectedComponent context={CustomContext1} />
          <ConnectedComponent context={CustomContext2}/>
       </Provider > 
 </App>
</Provider >

//where
const ConnectedComponent= connect(mapState,mapDispat)(Component)

So tbh, i think that 2x performance is better than have 2 identical unuseful way to do the same (rare-case) thing.

That's all.

@salvoravida We can't bump the peer dep without releasing as a new major. We would be breaking our API for users of React 16.4 or 16.5.

should not be a big problem use react 16.6 for 16.4 users if they get 2x performance, but i agree with you that choose the right version is not so easy for a common lib.

@alexreardon Could you make some tests ?