-
-
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
Add basic middleware api to default dispatcher #63
Conversation
@@ -5,7 +5,7 @@ export default class Redux { | |||
constructor(dispatcher, initialState) { | |||
if (typeof dispatcher === 'object') { | |||
// A shortcut notation to use the default dispatcher | |||
dispatcher = createDispatcher(composeStores(dispatcher)); | |||
dispatcher = createDispatcher(composeStores(dispatcher), initialState); |
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 wondering why you pass initialState
as second argument but in the createDispatcher
definition you expect a middleware
function?
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.
@emmenko It's because we're overloading the signature of the constructor. initialState
is actually middleware
, just like dispatcher
in that call is actually a stores hash. You make a good point, though... it should be the third argument so we can still set the initialState
. Eventually we'll need to rethink this (perhaps by using an options object instead) but for now we're trying not to break the existing API.
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.
Yeah ok I guessed that. And probably an options object would be better yes.
PS: we are already breaking the API ;) Better do it now then later when people already start using it :)
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.
Haha it'll change soon once we get to a stabler place, but in the meantime we don't want to be breaking the API for every new PR.
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.
Yep 👍
Does the default middleware need to be special? Can we let any middleware read state? |
IMO, If a middleware needs to read the current state, it should probably be implemented inside the dispatcher instead. Action middleware is just about transforming a stream of actions, nothing more. They should know nothing about the current state, unless of course it's passed in at the calling site. |
Also I'm not a fan of how the current |
If something isn't good, we should change it! But I think reading state in action creators is important. Putting a Redux instance into a separate module and calling There's definitely a problem with the current API though. The Here's what I propose:
Thoughts? |
Can you give me an example of when it's a good idea for action creators to read directly from the store? It sounds like a foot gun to me. If the call site has access to the |
Also, on a separate subject, staleness isn't an issue in the current version because the Edit: Never mind, you're right. |
What's footgunny about it? export function requestStarredReposPage(login, isInitialRequest) {
// Exit early if already fetching, or if there is nothing to fetch.
if (StarredReposByUserStore.isExpectingPage(login) ||
StarredReposByUserStore.isLastPage(login)) {
return;
}
// Ignore first page request when component is mounting if we already
// loaded at least one page before. This gives us instant Back button.
if (isInitialRequest && StarredReposByUserStore.getPageCount(login) > 0) {
return;
}
const nextPageUrl = StarredReposByUserStore.getNextPageUrl(login);
dispatchAsync(RepoAPI.getStarredReposPage(login, nextPageUrl), {
request: ActionTypes.REQUEST_STARRED_REPOS_PAGE,
success: ActionTypes.REQUEST_STARRED_REPOS_PAGE_SUCCESS,
failure: ActionTypes.REQUEST_STARRED_REPOS_PAGE_ERROR
}, { login });
}
That's not strictly true. The callsite is likely to be a React component. We currently don't provide a convenient way to read arbitrary state from a React component. You could use
But once it's passed to a function it doesn't matter right? I'm thinking of the case when one action creator |
Which part of your example couldn't be implemented by passing the state corresponding to the starred repo directly to the action creator?
Why does it need to be super broad? The call site can select only the subset it needs to perform the action, which in many cases should overlap with the subset it needs to render the view. But in any case, we probably should be providing an on-demand |
And yeah you're write about the state staleness, my bad. |
It's still cumbersome to duplicate the same “selection and early exit” code across the components, especially if the same data is being selected, but the prop shape is already slightly different in the components. I think it's good that one can specify that an action under some conditions doesn't make sense and shouldn't ever be called, in the action itself. This helps prevent incorrect action creator usage in a larger app.
Yeah, perhaps. |
Isn't that what helper functions are for? :) We already need to replace those store accessor methods that people are so fond of. |
You can still do that, you just have to pass the state in rather than accessing directly from the action creator. A |
If you're invoking an action creator from another action creator, or somewhere from server code, you need to remember to pass it. Sure, it's doable, but isn't it just on the same level of convenience as having a |
Maybe we can solve this with higher-level middleware: createDispatcher({
store,
middleware: getState => compose(performMiddleware(getState), callbackMiddleware)
}) |
What do you think about my suggestion in #63 (comment)? Keep it a normal middleware, but injected from Redux instance. This neatly sidesteps the problem because it already has access to the instance. |
Yeah I like that solution for the default middleware. What about custom middleware passed to |
OK that's fair. Let's give any middleware I don't like the additional “one level deeper” thing though. It feels more complex that it could have been. Why not change the middleware signature from |
Because then every middleware has to pass |
Also what if later down the road, we decide that we want to change the middleware signature once again? Then everybody in userland has to update their middleware. Whereas if we stick with the most basic signature possible, it's future proof, because the only thing that needs to change is the higher-order function. It's even Flux library proof, because middleware as I've described it will work with any dispatch method, not just Redux! |
Actually, it would be |
You convinced me, can you update the PR please? |
Sure thing, I get to it ASAP. |
What was your opinion about type-checking the middleware passed to |
I don't mind built-in array composition if I see a nice use case for it. |
I would leave it out for now and just make people specify a single middleware, but since the higher-order function and middleware are both functions, there's no way to use type checking to distinguish them. |
Right. OK, let's allow only |
👍 |
…and pass from Redux class instead
@gaearon Updated the PR according to our discussion. |
|
||
return recurse; | ||
} | ||
|
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.
Hmm, on second thought this really should be its own module, implemented using higher-order middleware.
I separated the default middleware to its own module. Now it's implemented just like any other middleware. // A shortcut notation to use the default dispatcher
dispatcher = createDispatcher(
composeStores(dispatcher),
getState => [ thunkMiddleware(getState) ]
); @gaearon What do you think of the name "thunk middleware"? It's more specific than "callback middleware," but I doubt many people are familiar with this term. I only know it because of co. Edit: To clarify, it's still the default middleware... I merely changed the implementation. |
“Thunk” makes sense to me. Since you don't have to specify it by default, I think this name fits well. |
dispatcher = createDispatcher(composeStores(dispatcher)); | ||
dispatcher = createDispatcher( | ||
composeStores(dispatcher), | ||
getState => [ thunkMiddleware(getState) ] |
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.
Nitpick: I'd prefer no spaces inside arrays
Add basic middleware api to default dispatcher
👍 |
Let's update the README and do a release after RN support lands. |
This gives users the ability to pass global "action middleware" (terminology still in flux) to the default dispatcher.
What is middleware?
Middleware is a function that wraps the
dispatch()
method, or another middleware. For example, to use a middleware:Multiple middleware can be composed manually
Or using the provided
compose()
utility:The
compose()
middleware may seem trivial, but it makes it allows you to easily compose an array of middleware using spread notation:Because middleware simply wraps
dispatch()
to return a function of the same signature, they can be used completely within userland. However, for the most part, you'll want to apply them globally to every action dispatch.Example of how to write middleware
Here's a middleware for adding naive promise support to Redux:
Use cases
Usually, they'll be used like schedulers. They can be used to implement promise support (a la Flummox), observable support, generator support, whatever. Or they can simply be used for side-effects like logging.
How this affects the core API
This PR does not break the default behavior of the existing API. For instance, while
perform()
has been re-implemented as middleware internally, it is the default middleware if none is configured by the user.In the future, Redux may provide some additional middlewares for things like optimistic updates, but they will be completely optional and not enabled by default.
Why is middleware special? Why not simply tell users to provide a custom Dispatcher and call it a day?
Custom dispatchers are the way to go for advanced functionality like time travel and transactions. But middleware is special because of a very important property: because middleware wraps the
dispatch()
method, it's inherently compatible with any dispatcher, regardless of implementation.Still, middleware is totally optional, and each dispatcher implementation can choose whether or not to support them. (At the global level, that is — middleware can always be used from the calling site.)