-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
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
Proposal: add subscribe
to middlewareAPI
#922
Comments
Not sure about this. My current thinking is that if middleware needs to subscribe, it should be implemented as a store enhancer. This is how Redux DevTools and Redux Router works. We haven't documented this pattern yet, but we likely will in the future. |
Example of a store enhancer: const storeEnhancer = createStore => (reducer, initialState) => {
const store = createStore(reducer, initialState);
// do whatever you want with store object
return store;
}; |
I agree with @acdlite on this. Middleware is meant to be “pass through” thing, anything more complicated should either be a store enhancer or just a function taking the store and calling |
@acdlite, @gaearon, I trust your judgement on this; but I have been struggling with this particular detail for a while; there are already examples of middleware doing more than just "passing through" actions, for example, the API middleware in the real-world example. From what I can see, Store enhancers do not have access to actions as they pass to reducers, so are therefore not in a position to intercept, sign and / or detain CALL_API actions when required by the app's business logic; likewise a function which wraps the store also has no way to access this flow - middleware appears to be the only way to tap into it. Exposing the |
@jonnyreeves The store enhancer has to pass the action to the reducer so yes, it is very much in the position to intercept actions. You can add enhancers before your middleware and after and even do things like middleware -> enhancer -> middleware -> another enhancer. You are in full control. So if you wanted to prevent the |
@johanneslumpe Thank you for the clarification; apologies for jumping to conclusions without reading the code. |
@jonnyreeves no worries :) |
@jonnyreeves It may help to know that |
Sorry to bring this up again; I believe the real problem I'm facing is that I need to trigger a side effect ( function) after an action has been reduced and the store's state updated. It would appear that I am in need of a side-effects processor, similar to #569, as a result I knocked up a simple proof of concept side-effect processor which looks for export default function sideEffectorExecutor(next) {
return (reducer, initialState) => {
const store = next(reducer, initialState);
let pendingSideEffects;
let unsubscribe;
// custom `store.dispatch` implementation which records side-effects on the
// action object and executes them on the next store update.
const dispatch = (action) => {
const { meta: { sideEffects } = {} } = action;
if (Array.isArray(sideEffects)) {
pendingSideEffects = sideEffects.concat();
// one-shot subscription to invoke side-effects after the next store update.
unsubscribe = store.subscribe(() => {
unsubscribe();
// process all side effects.
pendingSideEffects.forEach(sideEffect => sideEffect());
pendingSideEffects = null;
});
}
// Pass the action through.
store.dispatch(action);
};
return { ...store, dispatch }
};
} One issue I am facing with this approach is that I can't provide a meaningful |
Not sure if this is relevant, but I have thought about this a lot. There are a few times where I really do need to "wait" on an action coming through the system at a later time. And I don't know what started the I/O, so I can't block on the promise (by composing action creators), all I know is that it's already started and I need to wait for it to finish. I think I found a rather elegant solution to this, and I agree that we shouldn't (edit, I previously wrote "should") expose Basically, with this you can now dispatch an action that will fire something at a later time based on a predicate: const { NAME: waitService } = require('wait-service.js');
dispatch({
type: waitService,
predicate: action => (action.type === "ADD_ITEM" &&
action.status === "done"),
run: (dispatch, getState, action) => {
// dispatch and getState are what you expect. `action` is the
// action that fulfilled the predicate.
}
}); I have found that I've been to solve some really complex async scenarious with this simple service that has a little state: a list of waiting actions that check each action coming through the system. (I've been meaning to write a blog post about this) |
I may have mis-read the OP; looks like you just need the |
@jlongster, nope - you're spot on. I have middleware that contains state (detained actions) and I want to re-dispatch those actions when a predicate is met on the store. The later request for dispatch is in response to using a store enhancer to try and achieve the same effect. |
Now that I think of it.. The only way a store may change is by No? |
@gaearon but there is no guarantee that the action will hit the reducer after the This brings me back to why I wanted to subscribe to the store in the middleware, to know, for sure, that the store is in the correct state to proceed. |
If it doesn't reach the reducer, the state wouldn't get updated. I still don't understand the difference between “subscribing with a predicate” and “putting an action into an array, and next time an action is dispatched and the predicate is fulfilled, dispatch actions from array too.” |
This is the crux of the problem, the following code can not be relied upon: // This is the current state of the store.
assert.equal(getState().state, 'old_state');
// This action *should* update the 'state' property on the store.
dispatch(updateState('new_state'));
// So this assertion *should* be fine as the `updateState` action will have been reduced.
assert.equal(getState().state, 'new_state'); However, as per the redux documentation:
The action could well be delayed and the assertion above could fail. Currently, middleware has no way to observe the store, and therefore no way to know, for certain, when a given predicate can be met. I've created a github project which highlightes the issue I'm facing: https://github.com/jonnyreeves/redux-issue-922. The main actor is the auth-middleware. This is stateful, async middleware which will detain certain actions if the grant in the redux store has expired. In the real implementation, the detained actions are replayed once a valid grant has been obtained. If you Thanks. |
Yes, but then you should just warn the user to put middleware at the end of the middleware chain. Sure, it's not fool proof, but this is the best mechanism we got right now. Otherwise, feel free to use a store enhancer as described above. |
Thanks for your input @gaearon. I have solved my particular problem by creating a custom store enhancer: redux-action-side-effects which invokes an action's side effects after the action has been reduced. My authentication middleware then attaches a side-effect to the auth action as it passes through. I personally feel there is still a case for adding Thanks again for your hard work on redux. 🍻 |
Expand the existing
middlewareAPI
to includestore.subscribe
, ie:Motivation
Stateful middleware implementations wish to observe the store to know when the store' state has been modified and trigger a side-effect; for example - OAuth middleware will detain
CALL_API
actions when it detects an expired OAuth Grant until the store has been updated with a new, valid Grant:The text was updated successfully, but these errors were encountered: