question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Consolidated React 16.3+ compatibility and roadmap discussion

See original GitHub issue

We’ve currently got several open PRs and discussions going on around how React-Redux is going to interact with React 16.3 and beyond:

I want to try to pull together some of the scattered discussion and thoughts and lay out some of the things we need to address, so that we can figure out the best path forward.

React 16.3+ compatibility concerns

  • 16.3 adds support for the new lifecycle methods, but doesn’t actually start warning about them yet. It also adds <StrictMode> to check for async-unsafe usages in a subtree
  • 16.3 also adds the new context API. Old context will be removed in 17.0. Now that new context is available, we can use that instead, and that opens up a whole new set of possibilities for our internal implementation.
  • In order to be async-safe, we probably shouldn’t have any fields on the component instance any more. There may also be issues with the way the memoized selector logic works right now, because it’s running in componentWillReceiveProps, is stateful, and the calling logic relies on grabbing store.getState() every time.
  • connect() probably needs to use React.forwardRef() internally to allow easy access to instances of the wrapped component, and remove the need for getWrappedInstance()

Current PRs

We have three divergent 16.3-related PRs that rework connectAdvanced in separate ways:

  • #856:
    • drops the separate Subscription concept,
    • still uses old context
    • still subscribes to the store directly per component instance
    • removes makeSelectorStateful
    • adds a new shouldHandleSubscription value to let descendants know whether they need to subscribe themselves or not
    • uses functional setState to only return a state update if the selector indicates something changed
    • doesn’t change the lifecycle methods
  • #919:
    • still has separate Subscription handling
    • still uses old context
    • still subscribes to the store directly per component instance
    • reworks makeSelectorStateful into makeUpdater
    • fixes the lifecycle method warnings by adding getDerivedStateFromProps, using react-lifecycles-compat to polyfill that, removing use of componentWillReceiveProps, and keeping the updater function in component state, as well as changing the HMR subscription updating to use componentDidUpdate
  • #898:
    • drops the separate Subscription concept, and also removes the special HMR update handling entirely as it’s no longer needed
    • uses the new context API, creates a singleton Context.Provider/Consumer pair for internal use, and drops the old context API
    • changes from having every connected component instance subscribe to the store, to having only <Provider> subscribe, and put {storeState, dispatch} into the context
    • reworks makeSelectorStateful to accept the current store state as an argument instead of calling store.getState()
    • uses UNSAFE_componentWillReceiveProps and still runs the stateful selector there
    • still has the selector instance attached to the component instance
    • moves logic into a render prop callback for the Context.Consumer
    • currently loses the ability to pass the store directly as a prop to a connected component

Other Migration Concerns

Store Access via Context

There’s a bunch of libs out there that access the store directly as this.context.store and do stuff with it. This is most common for libs that try to add namespacing behavior, where they intercept the original store in context and pass down their own wrapped-up version that only exposes a certain slice via getState(), and automatically adds namespacing to actions. I’ve also seen it used for libs that allow dynamically adding reducers/sagas from rendered components. (Examples: redux-dynostore-react-redux, redux-fractal, react-component-chunk, this gist from Sunil Pai, and #948 ).

Now, accessing the store in context is not part of our public API, and is not officially supported. Any of those uses will break as soon as we switch to using new context instead. But, given that we’ve got these sorts of use cases out there, I’d like to figure out if there’s some way we can still make them possible, even if we don’t officially support them.

Passing a Store as a Prop

In addition to the standard <Provider store={store} /> usage, connect has always supported <ConnectedComponent store={store} /> to pass a store to that specific component instance. That worked okay because each component instance subscribes to the store separately. The changes in #898 make that a lot harder to implement, because now <Provider> is the only subscriber, not the individual components.

The simplest resolution would be to just drop support for passing a store as a prop going forward, and tell people to wrap that one component in another <Provider>, like: <Provider store={secondStore}><ConnectedComponent /></Provider> Since a React Context.Consumer grabs its value from the nearest ancestor instance of the matching Context.Provider, that second Provider would be used instead of the one at the root of the app.

However, that would be a difference in behavior from passing the store as a prop, because store-as-prop only applies the new store to that one specific component instance, not any of its descendants.

The primary use case I’ve heard of for store-as-prop is to simplify testing of connected components, but I’ve also seen mentions of plugin-type behavior that relies on store-as-prop (per discussion in #942).

A couple possible workarounds or solutions here might be:

  • Have <Provider> accept a Context.Provider instance as a prop and use that if available, rather than the “default” singleton instance. connect() might also want to take a Context.Consumer instance as a prop.
  • Possibly in conjunction with that, in order to make store-as-prop work only for that one component, a connected component could generate its own unique context pair and actually render a <Provider> inside of itself, so that only its own consumer gets updates from that store. Seems silly and hacky, but it’s the only immediate approach I can think of to keep the current semantics.

Actual React Suspense and Async Usage

Per discussion in #890 and #898, in the long term React-Redux is going to need to be rethought somehow to take full advantage of the async rendering capabilities React is adding. I wrote a summary of my understanding of the major issues, and Dan confirmed that was basically correct. Quoting:

To the best of my understanding, these are the problems that React-Redux faces when trying to work with async React:

  1. React’s time-slicing means that it’s possible for other browser events to occur in between slices of update work. React might have half of a component tree diff calculation done, pause to let the browser handle some events, and something else might happen during that time (like a Redux action being dispatched). That could cause different parts of the component tree to read different values from the store, which is known as “tearing”, rather than all components rendering based on the same store contents in the same diff calculation.
  1. Because of time-slicing, React also has the ability to set aside partially-completed tree diffs if a higher priority update occurs in the middle. It would fully calculate and apply the changes from the higher-priority change (like a textbox keystroke), then go back to the partially-calculated low-pri diff, modify it based on the completed high-pri update, and finish calculating and applying the low-pri diff. In other words, React has the ability to re-order queued updates based on priority, but also will start calculating low-pri updates as soon as they’re queued. That is not how Redux works out of the box. If I dispatch a “DATA_LOADED” action and then a “TEXT_UPDATED” action, the data load causes a new store state right away. The UI will eventually look “right”, but the sequence of calculating and applying the updates was not optimal, because Redux pushed state updates into React in the order they came in.

The changes to use new context in #898 appear likely to resolve the “tearing” issues, but do nothing to deal with the update rebasing behavior in async React, or use with React Suspense.

Use of New Context and getDerivedStateFromProps

One of the downsides to the new context API is that access to the data in lifecycle methods requires creating an intermediate component to accept the data as props (per React docs: Context#Accessing Context in Lifecycle Methods, react#12397, and Answers to common questions about render props ).

Some of the discussion in #890 and #898 included notional rewrites of connect() to use an additional inner component class. I can see how that might help simplify some of the overall implementation, but it adds another layer to the component tree, and I’d really rather not do that (if for no other reason than it adds that many more levels of nesting to the React DevTools component tree inspector).

There’s some open issues against the React DevTools proposing ways to hide component types (see React DevTools issues #503, #604, #864, #1001, and #997). If some improvements happened there, perhaps it might be more feasible to use this kind of approach.

Connection via Render Props

We’ve had numerous requests to add a render-props approach to connecting to the store, such as #799 and #920. There’s also been a bunch of community-written implementations of this, such as redux-connector (which had some relevant discussion on HN including comments from me).

Clearly there’s interest in this approach. I haven’t actually written or used anything with render props yet myself, other than this first use of Context.Consumer, but I know that apparently it’s possible to take a render-props implementation and wrap it in a HOC to make both methods a possibility.

Paths Forward

Tim ran a pair of Twitter polls asking if it was okay that React-Redux 6.0 only supported React 16.x, and if it went further and only supported 16.3+. In both cases, 90% of responses said “we’re already on 16.x / 16.3, or can upgrade”. Not scientific results, but a useful set of data points.

I’m going to propose this possible roadmap:

  • 5.1: Make additional improvements to the “remove async-unsafe lifecycle” changes in #919 so that there’s no use of fields on the component instance, then merge that in. That would make 5.x at least not throw warnings as React 16.x progresses, and would likely be the last meaningful release in the 5.x line.
  • 6.0: flesh out my “use new context” PR in #898, including fixing any lifecycle issues, and ship that as 6.0. This would require React 16.3+ as a baseline. That gives us better compat for async behavior overall, and simplifies the polyfill/compat story. We’d also use forwardRef() here, drop getWrappedInstance() from the API, and probably drop store-as-prop as well.
  • 7.0: a full-on re-think of the React-Redux API, taking into consideration things like React Suspense and async rendering, render props, and so on. Not sure what the final result here would look like.

Based on that, our story is:

  • If you’re on React 16.2 or below, stick with React-Redux 5.x. It still works fine, there’s no reason for you to change, and we’ve at least made it stop warning if you do bump up to 16.3+.
  • If you’re on 16.3+, go ahead and move up to React-Redux 6.x. Hopefully the switch from multiple subscriptions to a single subscription using new context will resolve the “tearing” concerns, and might even improve performance overall. This will require rewrites if you’re accessing the store via context or using store-as-prop / getWrappedInstance, but otherwise keep your code the same.
  • If you’re on 16.4+ and ready to move to the future, migrate to React-Redux 7.x once it’s out. You’ll probably have to rewrite your public usage of React-Redux, but once that’s done you should be able to get all the benefits of async React.

Thoughts?

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:53
  • Comments:22 (17 by maintainers)

github_iconTop GitHub Comments

3reactions
timdorrcommented, Nov 6, 2018

Closing this out since we’ve got this set up on master. It’ll be released soon-ish.

2reactions
markeriksoncommented, Sep 9, 2018

Tim put up a poll about a <Connect> / render-props-style API recently:

https://twitter.com/timdorr/status/1029451891082764290

Dead split 50/50 on whether people like it or not.

Read more comments on GitHub >

github_iconTop Results From Across the Web

React 16.x Roadmap – React Blog
As always, we will communicate our progress in the release notes on this blog. Status in React DOM and React Native: Technically, a...
Read more >
@rjsf/utils | Yarn - Package Manager
Refactored all themes to use the new @rjsf/utils library functions and types · Bumped most devDependencies to the latest versions where possible ·...
Read more >
تويتر \ Michael Holtzman (mikelax@) - Twitter
I just filed a React-Redux issue to consolidate the discussion around React 16.3+ compatibility, migration issues, and a possible roadmap going forward: ...
Read more >
instead of passing separate callback arguments, use an ...
reduxjs/react-reduxConsolidated React 16.3+ compatibility and roadmap discussion#950. Created over 4 years ago. 22. We've currently got several open PRs and ...
Read more >
2022 roadmap on neuromorphic computing and engineering
AlScN is a semiconductor processing compatible and already utilized ... relying on redox reactions and ion transport at extreme conditions [100].
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found