Consolidated React 16.3+ compatibility and roadmap discussion
See original GitHub issueWe’ve currently got several open PRs and discussions going on around how React-Redux is going to interact with React 16.3 and beyond:
- PR #856 : Experiment/POC: Optimize for use with React 16.
- Issue #897: Fix StrictMode Warnings
- Issue #890 : Discussion: React-Redux and React async rendering
- Issue #914: connect() does not work with React.forwardRef()
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 grabbingstore.getState()
every time. connect()
probably needs to useReact.forwardRef()
internally to allow easy access to instances of the wrapped component, and remove the need forgetWrappedInstance()
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
- drops the separate
- #919:
- still has separate
Subscription
handling - still uses old context
- still subscribes to the store directly per component instance
- reworks
makeSelectorStateful
intomakeUpdater
- fixes the lifecycle method warnings by adding
getDerivedStateFromProps
, usingreact-lifecycles-compat
to polyfill that, removing use ofcomponentWillReceiveProps
, and keeping the updater function in component state, as well as changing the HMR subscription updating to usecomponentDidUpdate
- still has separate
- #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 callingstore.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
- drops the separate
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 aContext.Provider
instance as a prop and use that if available, rather than the “default” singleton instance.connect()
might also want to take aContext.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:
- 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.
- 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, dropgetWrappedInstance()
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:
- Created 5 years ago
- Reactions:53
- Comments:22 (17 by maintainers)
Closing this out since we’ve got this set up on master. It’ll be released soon-ish.
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.