-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Alternate Proof of Concept: Enhancer Overhaul #2214
Conversation
…#1569) * throw error if getState, subscribe, or unsubscribe called while dispatching * prevent throwing if not subscribed * update getState error message * fix space after period * update subscribe/unsubscribe error messages
* Add a doc section on dispatching during middleware setup. * Warn when dispatching during middleware setup. * Upgrade the warning to an error. * Update docs to match thrown error behavior.
* Add mapped type for combineReducers in index.d.ts Updated typescript to 2.1.4 Updated typescript-definition-tester to 0.0.5 Updated typescript tests to use proper import Added mapped type to index.d.ts * add strict null check for reducer Updated Reducer<S> type in index.d.ts Add strictNullChecks flag to typescript spec
Behavior has been replaced with the "it warns when dispatching during middleware setup" test in the 'next' branch.
Definitely a lot to take in there. Per your last item: while there's 50+ store enhancers out there, there's several hundred middlewares, and effectively every Redux project out there uses one or more middleware.. I don't think the "turn middlewares into enhancers" idea has any feasibility whatsoever. Obligatory pinging of @gaearon , @acdlite , @timdorr , and all of @reactjs/redux for comment. |
} | ||
validateAction(action) | ||
currentState = store.final.reducer(currentState, action) | ||
store.final.onChange() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One request that keeps coming up redux-batched-subscribe is having access to the action
for applying some conditional logic on when to notify listeners. With the current onChange API this wouldn't be possible unless action
gets passed along as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tappleby : that's really related to the intent of the core Redux subscription API itself. There's been numerous requests since the beginning to add either the action or the current state as an argument to subscription callbacks. Issue #580 has Dan's reasoning and a roundup of almost all the other times this question was asked.
That said, I'm vaguely starting to wonder if we at least ought to re-discuss the idea for a notional 4.0 release. I'd have to go back and review all the prior discussions, but I'm not immediately seeing actual harm that might happen if we implemented the idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still agree with #580, this behaviour is actually what makes a library like redux-batched-subscribe (RBS) possible, if the action was passed to subscription listeners this would make it difficult to perform any debounce or batching logic.
Keeping consistent with this API was my main reason not implementing the feature in RBS yet. But based on the number of requests to support conditional batching logic based on the action + the majority of the forks existing just to add this feature, I am re-evaluating things.
Does passing the action "internally" within redux to onChange run into the same/similar issues #580 is trying to avoid by passing the action to external subscription listeners?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm on the fence. I think, outside of the store, app code shouldn't depend on what actions were fired. But within a store enhancer or middleware, perhaps this isn't as true. Most middleware already is just doing something based on what action was dispatched. Perhaps an enhancer overriding onChange is no different. The risk is if 2 or more enhancers override dispatch/onChange in incompatible ways, the more coupled the method signatures, the more likely of an incompatibility.
The reason it seemed dubious to me is I don’t think we even guarantee that all subscribers are called for all actions. I’m not sure I remember how we dispatch actions right now, but my understanding is if we dispatch action B while notifying subscribers about action A, we might as well notify the rest just about action B. It would be a bit odd to call subscribers twice (even if we do this now, not doing it seems like an optimization opportunity). The intended guarantee is that Redux always eventually (within the top-level dispatch) calls all subscribers with the most recent state, but not that it always calls each subscriber for each action. |
Regarding this PR. I really appreciate all the research that went into it, but this is not quite the direction I imagined for Redux. Right now Redux is not “integrated”, but at least its internals (and contracts) are simple. I’m worried this change makes it neither simple nor integrated, leaving it in an uncanny valley. The explicit goal here is to support existing characteristics of enhancers (such as mutating APIs) but I’m not sure they are actually good characteristics in long term. Maybe making some use cases “uglier” will force people to come up with better APIs instead of putting them on the store object. I’m not actively involved in Redux anymore so my opinion is probably biased and not very relevant though. |
Aw, c'mon, Dan. Even you're not actively working on Redux itself these days, your opinions and insights are always valuable, informative, and welcomed.
Can you clarify what you mean by "integrated" here? I've got a bunch of different things on my plate atm, so I don't have time right now to turn full attention to actual Redux change proposals. (Fortunately, there's no particular hurry). I'll toss out my general thoughts, though. As I've said numerous times, my default answer to proposed Redux changes is "no", unless it's fixing an actual bug or adding an actually useful capability. That said, now that the core library has been stable for quite a while, I do think it's worth seeing how the ecosystem has evolved and taking that into account (ie, the "worn path" approach to laying down a sidewalk). I think investigating ways to make the ecosystem's job easier is a worthwhile effort, and it's also worth going back to commonly asked-for features like "action/state as a subscription argument" to at least re-evaluate them in light of where things stand now. I'm not saying we automatically say yes to all prior suggestions, just that there might be some stuff that is worth implementing after all now that we understand the use cases better. |
Another problem with passing the action (sorry that this discussion is unrelated to the PR!) is that it makes it harder to add debouncing in the future. The moment action is available, your app (or a third party library) begins to depend on it, and now we can’t safely debounce and batch subscriptions. |
Yeah, @tappleby just mentioned that in another comment. And yes, that's a great reason to back up the current stance against adding that to subscription callbacks, and I'll make a note to add that to the eventual FAQ entry on the topic. |
fc0d7ae
to
a2200d5
Compare
Preface
This is an alternative solution to Proof of Concept: Enhancer Overhaul #1702 that attempts to achieve the same goals without constraining enhancers to a more limited API.
I've compiled a spreadsheet of store enhancer projects and examined most of their source code to get a sense of what those enhancers are currently doing.
Goals
Modern Enhancers
The major new concept is evolving the current enhancer API (which I will call "classic enhancers") to a new API (which I will call "modern enhancers"). Here's a simple example of a modern enhancer:
A modern enhancer accepts a store and returns a new partial store object with the properties it adds + overrides. Conceptually, a modern enhancer is similar to the Decorator design pattern. Even the core store functionality has been refactored so that it's described as several modern enhancers.
createStore
calls the sequence of enhancers to build the final store:This is the build phase of creating the store, in which all the enhancers add their behavior to the store. After the build phase, is the initialization phase, when
createStore
callsfinalStore.init({ reducer, preloadedState })
.New: store.init
Since a modern enhancer doesn't have access to the initial reducer or preloaded state as part of its main method, its opportunity to intercept those values is now done in
store.init
.init
is also the appropriate time to do things such as subscribing to the store or any external setup calls. For example redux-persist's autoRehydrate enhancer would change from this:To this:
New: store.final
By making
createStore
control the entire build process of the final store, the "magic" feature this enables is allowing all the enhancers to have a reference to the final store. This feature was inspired byapplyMiddleware
, and mechanically it works the same way. For example, the base store'sdispatch
method looks like:The base
dispatch
method invokes the final versions ofreducer
andonChange
, not just those methods as defined on the base store. This allows other store enhancers to override the default implementations called bydispatch
. Currently in 3.6, overridingreducer
requires wrapping it duringcreateStore
call and alsoreplaceReducer
. The logic foronChange
is not overridable in 3.6 and requires an enhancer to wrapdispatch
andsubscribe
.New: store.reducer
A common pattern among existing enhancers is wrapping the reducer in a new function, while calling
createStore/next
and also inreplaceReducer
. This pattern can be simplified by puttingreducer
on the store. For example, here is the current version of install from redux-loop:And converted to a modern enhacer, taking advantage of the ability to override
reducer
:New: store.onChange
Another desire of some store enhancers is the ability to control when subscriptions are notified after actions are dispatched. In order to make that simpler, base
dispatch
now invokesstore.final.onChange()
. Additionally the listener subscribe/onChange logic has been extracted into its own filecreateEvent
. As an example, this allows redux-batched-subscribe to go from this:To this:
Backwards compatibility
Some extra API is needed in order to prevent/minimize disruption of any API changes for application developers. First, because applications are using
compose
to combine enhancers and passing the result of that tocreateStore
as the 3rd argument, an adapter is needed to make a modern enhancer backwards compatible. Two methods,adaptEnhancer
andadaptEnhancerCreator
are provided to make a modern enhancer match the signature of a classic enhancer.Wrapping a modern enhancer would look like:
Wrapping a modern enhancer that has a factory function would look like:
Going Further: Simplifying Redux with breaking changes
Some things not part of this PR, but worth discussing...
Good idea: Drop support for
compose
+ classic enhancer syntax completelyThe need for
adaptEnhancer
,adaptEnhancerCreator
, andcompose
could be removed ifcreateStore
drops support for the classic enhancer syntax. Some suggestions:The benefits of doing so would be a decent reduction in code and the number of concepts that need supported/documented.
Moonshot: Middleware are made redundant by store.final and could be phased out
Redux could be made conceptually simpler if all middleware are then converted to store enhancers.
For example here's redux-thunk as middleware:
And here's redux-thunk as an enhancer:
The enhancer version has the advantage that
store.final.dispatch
referencesdispatch
beyond just the one created byapplyMiddleware
; it is less "sandboxed." An ecosystem-wide refactoring to accomplish this for all middleware would be a very labor intensive operation, so I'm not sure it's worth it. Maybe suggest new projects write code as enhancers instead of middleware?Wrapping up
Much like #1702, this is a pretty big change, in mostly the same ways; and most of the main bullet points from that one still apply. The key difference is this solution doesn't limit the enhancers ability to add/override properties to the store, which a good number of enhancers do.