-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consolidated React 16.3+ compatibility and roadmap discussion #950
Comments
I've actually just started with a PoC for what 7.0 could look like. It's only about 30 lines of code, but leverages the new context API and uses a render prop (or cloneElement's a single child; neat!). I still have to implement state selection and action creators, but it's pretty deceptively simple right now. While a bit more verbose on the user's end, there's far less hand-wavy magic going on and I think that tradeoff will be better for everyone. Bonus points: it should be async compatible, though I need to stress test that later on. |
Interesting read, thanks for sharing. Personally I feel the current version of react-redux is more or less "done". No need for any new features. So I hope you're able to create a new version that leverages the new capabilities of React without being hindered by trying to support all the community driven solutions out there. If people still need those they can stay on the version of react-redux they are already on (great if it doesn't throw warnings on 16.x tho of course). At the same time, the community is part of what makes redux great, so I see the conundrum. One great part of redux and react-redux is the simple API tho, so I guess keeping the API surface as small as possible going forward as well should be a high priority. Regarding connecting with the render-props pattern, I've found this simple trick to be useful in the cases where I've wanted that, so don't necessarily feel it is something react-redux itself needs to support. Thanks for all the awesome work on Redux! It's a great time to be a front-end developer :) |
Hi, author of redux-subspace and redux-dynostore here. @markerikson has repeatedly mentioned (read: blamed 😛) my libs as concerns for removing the store from the context entirely, so I wanted to jump into this conversation and help come to a conclusion on how we can maintain support, but still allow Redux to push forward and use all the fancy new feature React is offering us now (or soon). I don't have much to offer, other than I am happy to answer any questions on our use case(s), why we did what we did, how a particular solution might work for us, or anything else you want to ask. |
FWIW, I know I've seen quite a few others that do similar things - yours just happen to be the ones that I remember the most and can point to :) I'm starting to think that maybe we ought to still put the store into context, and if someone wants to intercept it and pass some kind of overridden version down, that's up to them. We'd still put the store state into context as well separately. |
Selfishly, that is the easiest for us to upgrade to. Would it make sense to have a seperate provider/consumer context pair for the store and have |
hope for api like this: <Reudx.Provider store={store}>
xxx
<Redux.Consumer mapStateToProps={state => ({count: state.counter.count})} mapDispatchToProps={dispatch => ({ onInc: e => dispatch({ type: 'COUNT_INC' }) })}>
{({ count, onInc }) => xxx}
</Redux.Consumer>
xxx
</Redux.Provider> or even just: <Reudx.Provider store={store}>
xxx
<Redux.Consumer selector={store => ({count: store.getState().counter.count, onInc: e => store.dispatch({ type: 'COUNT_INC' })})}>
{({ count, onInc }, store) => xxx}
</Redux.Consumer>
xxx
</Redux.Provider> |
{({ count, onInc }, store) => xxx} I don't get the point for passing store as the second argument. |
It's for people who is lazy to write |
And in fact, the <Redux.Consumer>
{store => {
const count = store.getState().counter.count;
const onInc = e => store.dispatch({ type: 'COUNT_INC' });
return <div onClick={onInc}>{count}</div>
}}
</Redux.Consumer> I don't really know which is better. I just think we can use redux in the render-props way. |
Maybe we can use function render(store) {
return xxx;
}
function component({ store }) {
return render(store);
}
<Redux.Consumer selector={xxx} component={component} />
// or
<Redux.Consumer selector={xxx}>{render}</Redux.Comsumer> fell free to ignore this. |
Regarding v7.0, based on previous open source experience, I highly recommend forking redux to redux2 or something, and provide a new unified way to access the store with redux or redux2 that third party libs can use to access it, and reach out to the most important ones with PRs. This way you can make a clean break to re-imagine how mapping state to the tree changes with React 16. I'll be watching closely, since my router ion-router is piggybacked on redux. Trying to maintain backwards compatibility directly doesn't really help anything in the long run, and in many cases in the short run. Better is to write the best thing and make migration easy (codemods anyone?) In terms of responding to the new context model, what stops redux2 from putting the state in the root component (a context provider) using React state, and using context consumers to pass sub-trees of the state around? The redux consumers could accept "filters" to prune the tree to relevant portions needed for the component, and then re-rendering based on state change would be dead simple. Handling side effects and mutation in the new world will need some time to figure the new async world out, I think. But it would be a good opportunity to look at the situation afresh! Glad you're doing this Mark, thank you. |
I disagree with having a new package, I think that would prevent people from adopting it. For example Koa originally started as plans for Express 5, but they decided to make a completely new package and project because of how much it affected back compatability. Unfortunately the popularity and community of Koa is still much lower than Express', despite Koa having a superior design and having the ability to do anything Express can in theory. Koa is significantly more difficult to adopt because the Express community didn't break back compatibility, which allows innovation while keeping related projects in sync over a single standardized API (even if the new API requires a lot of work to adopt, it's better for end users). Let's keep the same package, even if it involves breaking back compatability for every single API. We could make the React specific version of Redux replace react-redux, or it can replace Redux itself and people can use a legacy version of Redux or a separate redux-legacy package if they aren't using React (Vue, backend only JS, non-web applications like Hyper, etc.). |
@cellog : a couple points of clarification. First, the discussions here are primarily around React-Redux, not the Redux core. My ideal goal is that the Redux core won't need to change, and neither would the main React-Redux API (at least for a notional v6 that primarily is about compat with async React). Second, maintaining backwards compatibility is very important for the immediate future. We've seen how things go when widely-used libraries or frameworks suddenly announce that everything is changing, and that's not something I want to do to Redux users. Again, I'd love it if a React-Redux user could just do Third, your "context consumer and filters" sounds like... uh... Looking at your specific lib, I see that you've got a store enhancer that adds extra fields to the store object itself, and uses context to access the Redux store directly. That's the kind of use case I'm concerned about, and want to find ways to support as we switch over to the new React context. |
My original train of thought for using new context would be that we'd only put the current store state and |
clarifying myself: "context consumer" was referring to React I was simply referring to the way redux interfaces with react in that it seems with react 16 and soon 17, it will be easier to maintain state without having to hoist it outside of react, and redux can use that strength. It is good to maintain compatibility when it makes sense. Not disputing that. I'm simply questioning if the under-the-hood details really need that, or if providing a clear upgrade path for libraries that use under-the-hood stuff (like mine) is better. I'm also questioning if redux's real strength is its framework-agnostic nature. Do you have stats on how many people use it outside React? As for my router, I will wait to see what you choose, then update based on that, like a good citizen :) |
I don't have specific stats, tbh, but I see no reason to suddenly make the core React-specific. Bindings layers for all the other major frameworks exist, and I don't want to make those impossible. Similarly, we've got a ton of example code that shows "hey, all you need to use Redux is one script tag and vanilla JS" - even if that's only a teaching use case, there's no reason to kill that off. |
FYI, I just converted ion-router to use the new context, and it gives me a bit more to work with. There is only one show stopper for me regarding the new context providers:
So I can't release my work. I'm currently updating tests to match the new API anyways, so it's not huge, enzyme does seem to be within a few weeks of a release to support the new context, and fragments. But the advantages are huge. I simply converted my top One of the big benefits I see from the new context design is that sub-apps with their own redux context can work without the need for a store key. The sub-app simply takes the provider that is closest in the higher part of the tree. In any case, I am in favor of 7.x passing the whole store to the context consumers. This will allow store enhancers and middleware to work without modification, and vastly simplify extending react-redux. Obviously, I haven't looked at the async issues, and that is going to require a larger re-think. |
There's a couple React PRs that will likely be very relevant to v6.0:
The context-reading API is probably going to be necessary if we want to stick with a single wrapper component for |
Pasting comment from #980 : After discussion with both @cellog and @timdorr , we're leaning towards not putting out a 5.1 release. Trying to manage lifecycle behavior across multiple React versions is difficult (and apparently the I'm currently trying to work on putting together a performance benchmark harness so that we can start comparing comparing behavior between different releases. From there, I plan to pick up work on #898 , or at least a variation of that approach, and start serious development on a 6.0 release that would probably be 16.5+ only. |
Tim put up a poll about a https://twitter.com/timdorr/status/1029451891082764290 Dead split 50/50 on whether people like it or not. |
Closing this out since we've got this set up on master. It'll be released soon-ish. |
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
<StrictMode>
to check for async-unsafe usages in a subtreecomponentWillReceiveProps
, 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:Subscription
concept,makeSelectorStateful
shouldHandleSubscription
value to let descendants know whether they need to subscribe themselves or notsetState
to only return a state update if the selector indicates something changedSubscription
handlingmakeSelectorStateful
intomakeUpdater
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
Subscription
concept, and also removes the special HMR update handling entirely as it's no longer neededContext.Provider/Consumer
pair for internal use, and drops the old context API<Provider>
subscribe, and put{storeState, dispatch}
into the contextmakeSelectorStateful
to accept the current store state as an argument instead of callingstore.getState()
UNSAFE_componentWillReceiveProps
and still runs the stateful selector thereContext.Consumer
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 viagetState()
, 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 ReactContext.Consumer
grabs its value from the nearest ancestor instance of the matchingContext.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:
<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.<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:
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:
forwardRef()
here, dropgetWrappedInstance()
from the API, and probably drop store-as-prop as well.Based on that, our story is:
getWrappedInstance
, but otherwise keep your code the same.Thoughts?
The text was updated successfully, but these errors were encountered: