Thunks are easy to use and a good default, but the biggest weakness is that they don't let you respond to dispatched actions. Sagas and observables are very powerful (_too_ powerful for most apps), but they do let you kick off additional logic in response to actions.
I've been considering adding some kind of middleware that would be in between - something that lets you run callback functions in response to specific actions, but without the complex overhead of sagas and observables.
I have links to a bunch of existing middleware under the Middleware and Middleware - Async sections of my addons list. One of those might do what we need, or we can take inspiration.
Glitch appears to have one at handler-middleware.js , which maybe @modernserf could say something about.
This is how I described this approach to my coworkers:
There are essentially two schools of thought for working with side effects with redux.
The first approach is to put the side effects in the actions -- e.g. instead of dispatching a simple value, dispatch a promise, or a function that calls side effects. Middleware like redux-promise and redux-thunk intercept these non-value actions and run them.
The second approach is to put the side effects directly into the middleware -- the actions are always simple values, but the middleware listens to these actions, and can run side effects in response to them. This is the approach used by redux-saga and redux-loop; it's also how logging and metrics-reporting middleware work.
I prefer the second approach because it encourages you to keep the "how" with the state, and the "what" with the UI. However, the tools associated with this approach, particularly redux-saga, tend to have a pretty steep learning curve. The approach I'm using here is less expressive, but much closer to the complexity of redux-thunk.
Yep, that's the idea I'm going for here.
One possible improvement I'd like to consider is adding and removing subscriptions at runtime. The likely approach would be dispatch(addActionListener(type, callback)).
What's the status now? It seems to me that the extraReducers can be used to respond to dispatched actions. Or are you working on something else?
What's the status now? It seems to me that the
extraReducerscan be used to respond to dispatched actions. Or are you working on something else?
Keep in mind, these are still reducers and should not have any side-effects.
But yes, API-wise I guess it will go in a similar direction as extraReducers, but we have no definite design yet. Suggestions welcome.
@phryneas
Yes, what I mean is I can respond to dispatched actions to change state in different slices, which is sort of listening behavior. I have seen the listener pattern in easy-peasy, here is the doc. Maybe you will have some inspirations from it.
By the way, I do think it's a very important feature to be included in redux-toolkit.
@phryneas
Yes, what I mean is I can respond to dispatched actions to change state in different slices, which is sort of listening behavior. I have seen the listener pattern in
easy-peasy, here is the doc. Maybe you will have some inspirations from it.By the way, I do think it's a very important feature to be included in redux-toolkit.
Yes, this is a feature to make this reducer to react to actions from a different slice/other action. I hope the current documentation reflects that. this part of the StyleGuide should also reflect that we encourage that pattern.
What this is not is a feature to trigger asynchronous actions (thunk-like) from anything. I just assumed you were talking about that, because this whole issue is about a middleware for asynchronous actions.
@phryneas I wasn't aware that this issue is about asynchronous actions. I think a listener pattern is all about do something when some action is dispatched, no matter it is a synchronous or asynchonous action. So if we have the listener pattern implemented, the extraReducers syntax is no longer needed.
@phryneas I wasn't aware that this issue is about asynchronous actions. I think a listener pattern is all about do something when some action is dispatched, no matter it is a synchronous or asynchonous action. So if we have the listener pattern implemented, the extraReducers syntax is no longer needed.
No, because an asynchronous listener cannot change state. Only reducers do. These are completely different responsibilities.
An asynchronous listener could only dispatch actions that finally might lead to state change.
The distinction is:
Yeah, per my original comment, the one missing use case with RTK atm is that there's no built-in way to kick off additional logic in response to dispatched actions. Sagas and observables let you do that, but they're both very heavyweight in terms of bundle size, complexity, and mental overhead.
Given that, I'd like to provide a much lighter way to let people run simple callbacks, probably with access to dispatch and getState like thunks or something.
Yeah, per my original comment, the one missing use case with RTK atm is that there's no built-in way to kick off additional logic in response to dispatched actions. Sagas and observables let you do that, but they're both very heavyweight in terms of bundle size, complexity, and mental overhead.
Given that, I'd like to provide a much lighter way to let people run simple callbacks, probably with access to
dispatchandgetStatelike thunks or something.
When(roughly) would we expect to see this feature?
No concrete plans or timing atm. Like, it's a thing I want to do, but I'm focused on docs work right now. (Or, more accurately, I'm on business travel for the next few weeks, and don't have much time to do either docs or coding).
If someone would like to pitch in and help work on this, I can help steer the work in the direction I'd like.
If someone would like to pitch in and help work on this, I can help steer the work in the direction I'd like.
You know the drill, give me a direction and I'll try some things out ;)
The question is: do you want to do this
createActionListenerMiddleware? configureStore? createSlice (which I think would be appealing, but people would have to manually add all those middlewares in configureStore unless we added some magic slices argument to configureStore that combined rootReducer and middlewares - or some dev mode runtime checks if a slice reducer is used without it's middleware).Yeah, this does go off into some larger API design questions than I think I'd originally realized.
Thinking through possible requirements a bit:
"listeners/addListener" action that contains a callback function as the payload, and is explicitly stopped by the middleware from proceeding to the reducers. There should also be some way to remove a listener using the same approach, whether it be based on a returned ID or a function reference equality check.{dispatch, getState} object into the callbacks. Perhaps we should also pass in {addListener, removeListener} as part of that param as well.But, from there, I'm not sure where and how all the listeners should be defined and pulled together.
The Glitch middleware allows you to pass in multiple handler maps at middleware creation time. That's not a bad initial approach, especially if there's the ability to dynamically add and remove listeners at runtime.
I don't think I want to define a middleware per slice as you have in the initial PR.
If we assume we add some kind of a listeners field to createSlice, how is the user supposed to refer to action types that are simultaneously being defined over in reducers? What if they want to have an action type that's "just" being used to trigger some logic - should they be generating that by hand via createAction instead? Is there a way to distinguish whether a listener is referring to a newly generated action type from reducers, vs one that was defined separately via createAction?
Also, how should the various listener definitions be pulled together? Is it sufficient to just include them in the slice object as it's created, and have the user call:
configureStore({
reducer,
middleware: [...getDefaultMiddleware({
listeners: [sliceA.listeners, sliceB.listeners]
})]
})
by hand, or should there by some other approach? I'd still prefer to not have any kind of a special "combine slices" function or anything like that.
This also goes off into the "larger Redux abstraction" land occupied by Rematch, Easy-Peasy, and Kea. It would be worth reviewing their APIs to see what the capabilities are, what the API looks like, and how it's implemented:
Overall, I think there's value in this idea, but I also want us to come up with something that's reasonably scoped and doesn't turn into a monster.
Hehe, while you've been writing here, I've been writing some of the same questions in the Draft PR :D
While I see the negatives, I also see one big positive of adding it to the slice: the logic stays much more together. If we could solve those problems, I think that could be a big plus, keeping it more "ducksy".
As for the dynamic action subscription via action you are suggesting, I see problems with that.
One is the same problem I have with thunks: it leads to people potentially littering their async logic everywhere. But while a thunk is only dispatched once, subscriptions that aren't rigirously unsubscribed can also start leaking. And there's no real way to notice that as there would need to be support for that from the devTools or something like that.
The other problem I see is much more pragmatic: you need an "unsubscribe" mechanism. Doing so by action name is not possible, as it might unsubscribe other subscribers. Doing so by function reference might also not be feasible, as it prevents writing inline functions or functions referencing the outer scope of a React Function Component. Leaves returning an unsubscribe method from dispatch. That might get overridden by another middleware.
So if there were to be a dynamic subscription mechanism, I'd put it as a method on the enhanced store. Either as a replaceListeners method (could be a callback that receives all current listeners and returns a new object of listeners), which would discourage "too wild" subscribing because it isn't that easy to use, or just as a subscribeAction method next to the subscribe method with similar API. That could also be used from components if really necessary because there's the useStore hook.
It'd be easy to have the middleware generate a unique ID when you ask to subscribe and return that from dispatch(), and use that for the unsubscribe.
But yeah, I can also see potential pain points from managing that as well.
It'd be easy to have the middleware generate a unique ID when you ask to subscribe and return that from
dispatch(), and use that for the unsubscribe.
Yeah, returning an unsubscribe function would also be no problem there. But I've been bitten by shoddily written middlewares throwing away return values from other middlewares in the past, so I've come to the point where I never trust the return value of a dispatch - especially not for something so important.
The other question that comes up here: _should_ there be a way to define thunks as part of a slice? If so, how?
The other question that comes up here: _should_ there be a way to define thunks as part of a slice? If so, how?
Phew. I'd say no, because it does not provide any extra value. Where the slice reducers/extraReducers build you a reducer and listeners would build you a middleware (and they all benefit from some kind of map object or builder notation), thunks would still be thunks. I'd encourage people to put thunks into the same file as the slice, though (as long as size is manageable).
But I see where you're coming from.
Any update on this? I think this is a very important feature as there is currently no good way to do middlewares in @reduxjs/toolkit.
Any update on this? I think this is a very important feature as there is currently no good way to do middlewares in
@reduxjs/toolkit.
configureStore has a middlewares argument, if you have problems with TypeScript here are instructions for that.
Is there anything else missing?
Any update on this? I think this is a very important feature as there is currently no good way to do middlewares in
@reduxjs/toolkit.configureStore has a
middlewaresargument, if you have problems with TypeScript here are instructions for that.
Is there anything else missing?
Yes, but this does not play nicely with actionTypes. We are using middlewares to sync data to DB on some actionTypes. We will listen for them like this:
export const syncToDb = api => next => action => {
switch (action.type) {
case actionTypes.RANDOM_ACTION: {
....code here
}
}
}
I see 2 problems here:
string which breaks Typescript inferring type of the payload inside switch case statementsMaybe using a type guard instead of a discriminating union will help you there?
Alternative to using a literally-typed action.type
So instead of a switch/case, you'd go if/else if /else... with actionCreator.match.
If you are using the discriminating union pattern, you are essentially lying to your compiler, as there is actually no guarantee that only actions you know of will be passed as an argument to your reducer/middelware.
There will definitely always be the redux-internal INIT action which has a random type, and many external redux-specific middlewares like redux-connected-router or redux-persist add their own internal actions that you as a developer don't know about. But they will definitely be passed to your Reducer/Middleware.
This looks good. Does it work with createSlice as well, or do the actions have to be manually created outside in this case?
This looks good. Does it work with
createSliceas well, or do the actions have to be manually created outside in this case?
Works with createSlice like a charm, as that uses createAction internally ;)
This looks good. Does it work with
createSliceas well, or do the actions have to be manually created outside in this case?Works with
createSlicelike a charm, as that usescreateActioninternally ;)
So you can do something like this?
mySlice.actions.myAction.match(incomingAction) {
// incomingAction.payload <-- is typed here
}
Yes.
I'm pretty late to this discussion, but I've read over the comments here and the #272 PR and I've got some thoughts.
getDefaultMiddleware. Ideally the listener's middleware would be automatically included in the defaultscreateSlice API. To me, they're more like middleware and/or thunks in that they exist as a seperate concept to the statethunk like API would be nice as it will be easier to teach in light of thunk also being included in the default layoutredux-thunk, there may be some use for this to others that already have a custom store setup, have outgrown RTK (if that's possible) or trying to move towards itMy ideal API would look something like:
import { createAction, createSlice, createListener, configureStore } from "@reduxjs/toolkit";
const triggerAction = createAction("triggerAction");
const slice = createSlice({
name: "example",
initialState: {},
reducers: {
something(state, action) {},
somethingElse(state, action) {}
}
});
const listener = createListener({
[triggerAction](action, { dispatch, getState }) {},
[slice.actions.something.type](action, { dispatch, getState }) {}
});
const store = configureStore({
reducer: {
slice: slice.reducer
},
listener
});
listener.add({
[slice.actions.somethingElse]: (action, { dispatch, getState }) {}
});
listener.remove(triggerAction.type, slice.actions.something);
Note: in this API listener can be any function with the signature function(action, { dispatch, getState }) and createListener is a helper similar to createReducer that simplifies the action filtering automatically and provides the add and remove functionality based on action type.
I haven't put too much thought into edge cases or usage beyond trivial examples, and I'm not familiar at all with the RTK implementation (although I'm very familiar with the internals of Redux itself), so it might be all kinds of broken.
I'm happy to take a run at a PR if others like this API.
Think I'd prefer a slight tweak to that api:
const [addListener, removeListener] = createListener(
'ACTION_TYPE' || selector,
(actionOrState, { dispatch, getState }) => {
// do stuff
}
})
const store = configureStore({
listeners: [addListener]
});
createListener would return action creators, which you can either dispatch or pass to configureStore (which would add the listeners initially). One important change is that you'd be able to listen to selectors too; having worked with Refract for a while, this would be my main request for this feature!
Couple of things which wouldn't be possible with this api:
This would be super simple to implement on top of Refract, but I'm guessing a new dep wouldn't make sense? 馃槣 Would be pretty easy to pull out the key pieces of code into a new enhancer/middleware though!
Hmm. This touches something I'm personally very undecided about, so this is very unfiltered brainstorming from my side here:
Dynamically adding and removing action listeners.
While this might surely have a lot of valid use cases, it would also lead to one use case that gives me a little bit of a headache:
React components "subscribing" on Redux actions being thrown.
Not only does this couple react components very tightly to store logic, essentially reducing redux to a CQRS stream, but it also brings all the problems like the need for properly unsubscribing on unmount or facing various memory leaks etc.
Even redux-observables says in their Usage with UI Frameworks
Generally speaking, your actual UI/templating logic (Angular/React/Vue/etc) should have no knowledge of redux-observable.
And allowing for any way of dynamically adding listeners would essentially be a "redux-observables from everywhere light".
So, I think we could go three ways here:
replaceReducers, but replaceListeners to create the ability to do so without making it too obvious of a patternIn the latter case, I'd suggest to open a whole new can of worms for RTK: add react-redux as optionalDependencies and provide a tree-shakable helper hook that handles adding action listeners (and that properly removes them on onmount).
Because that would at least allow it for us to provide a least-dangerous way of doing that.
But I guess, first you'd have to convince me that allowing this is a good idea at all.
Skimming through these last few comments. Good discussion.
Some quick thoughts:
createSlice or configureStore for now. getDefaultMiddleware would be the initial supported approachdispatch({type: "actionListeners/addListener", {predicate, callback}), where perhaps the predicate accepts both (action, state). I think that provides the most flexibility. Only thing is we'd need to figure out a way to convince the serializability check middleware to ignore that action type by default since we're sticking functions inside :) (which is a legit thing in this case since we know they'll never reach the reducers.)* My notional API leans more towards `dispatch({type: "actionListeners/addListener", {predicate, callback})`, where perhaps the predicate accepts both `(action, state)`. I think that provides the most flexibility.
What's your thought on "people are starting to listen to redux actions from react components"? This is something that will happen if we allow for this, so it might be good to think about our stance on that pattern beforehand.
Only thing is we'd need to figure out a way to convince the serializability check middleware to ignore that action type by default since we're sticking functions inside :) (which is a legit thing in this case since we know they'll never reach the reducers.)
Just put the new middleware before the serializability check middleware and not forward the actions any further in the middleware?
* I don't feel that adding listeners _only_ on store creation is sufficiently flexible
Also: what would be your stance on going one step less and add a replaceListeners method on the store? Could take a callback function that takes the current listeners, so you could add and remove some - but it wouldn't be as exposed as subscribing with actions.
Just put the new middleware before the serializability check middleware and not forward the actions any further in the middleware?
Oh duh. Why was I getting confused on that part?
I sorta see what you're saying about "listening to actions from React components", but I'm not overly concerned about that. If there's consensus that it's a bad idea, we can put a note in the docs that says "hey, even if this is possible, we recommend against it because..." and give some specific reasons.
I don't think I like that replaceListeners idea. Don't think it provides any real benefit.
Listening to actions in react components can be a really nice pattern for separating your code fwiw - much easier to code split some complex logic and embed it into view chunks if you're able to do that. Side-effects based on changes to state are also super useful, shame that it sounds like the api direction won't support that. 馃槙
@thisRaptori : the "predicate API" I proposed would likely allow that:
type ActionListenerPredicate = (state: State, action: Action) => boolean
Oh huh missed that part somehow! 馃憖
If we're ensuring these actions to add/remove listeners don't reach the store, what's the advantage of having them be actions at all? Building an enhancer instead of a middleware (which would then add an addListener method to the store rather than adding extra special cases into action dispatching code) feels like it might have fewer edge cases and be a bit more flexible... 馃
馃憢I thought I'd mention that (ages ago) I built something similar for myself which addresses some of the points mentioned in this issue and could be of inspiration (though you're welcome to ignore it too!).
@thisRaptori : think you're misunderstanding things a bit.
Of course the actions reach the _store_, otherwise they'd be completely useless. The point is they don't reach the _reducers_. I just wrote an extensive Reddit comment talking about how that's an intended use case for middleware.
The problem with making it an enhancer that adds an extra method to the store instance is that now you need actual access to that store instance in order to call it. That means either directly importing the store (a no-no) or calling the useStore() hook in a component (technically a thing you can do now, but discouraged).
On the other hand, everything has access to dispatch(), with no extra work needed.
@jameslnewell : yep, I have that one listed in my Redux addons catalog already :) along with a bunch of other similar libs:
@markerikson yeah I understood, I guess I just fall into the camp of feeling pretty uneasy about opaque middleware which intercepts actions like that. Know it's something which has been (more or less) embraced, but I'm still not quite convinced it's a net positive.
Yeah I was assuming it'd be exposed via a custom useListener() hook which would call useStore() internally - was imagining it'd be a separate lib or something since RTK is meant to be view library agnostic. Also personally really dislike dispatch being everywhere, but hey not a hill I'm interested in fighting over let alone dying on! 馃槢
RTK is meant to be view library agnostic
Right, this is another reason why I would want a dispatch-based mechanism.
ok, just to confirm @markerikson, you're version looks something like:
import { createAction, createSlice, createListener, getDefaultMiddleware, configureStore } from "@reduxjs/toolkit";
import { addListener, removeListener } from 'redux-action-listener' // package name to be determined
const triggerAction1 = createAction("triggerAction1");
const triggerAction2 = createAction("triggerAction2");
const slice = createSlice({
name: "example",
initialState: {},
reducers: {
something(state, action) {},
somethingElse(state, action) {}
}
});
// can construct with action creator or action type
const listener1 = createListener(triggerAction1, (action, { dispatch, getState }) => {})
const listener2 = createListener(slice.actions.something.type, (action, { dispatch, getState }) => {})
const store = configureStore({
reducer: {
slice: slice.reducer
},
middleware: getDefaultMiddleware({
listeners: [listener1, listener2]
// other options take objects, but not sure it makes sense here...
// perhaps `listener: { listeners: [...] }`?
// I've got an idea that they could accept an `extraArgument` similarly to `redux-thunk`
// so having the ability to extend the config might be useful
})
});
// can add by action creator or action type
store.dispatch(addListener(createListener(triggerAction2.type, (action, { dispatch, getState }) => {}))
store.dispatch(addListener(createListener(slice.actions.somethingElse, (action, { dispatch, getState }) => {}))
// can remove by action creator or action type
store.dispatch(removeListener(triggerAction1.type))
store.dispatch(removeListener(slice.actions.something))
My only concern with this approach is that it's the only thing in the standard setup that forces you to use getDefaultMiddleware to opt into it. Although I guess you could just dispatch your initial listeners immediately after creating the store if you _really_ didn't want to use it, I don't think that should be the encouraged approach. I could probably get on board with the idea that if you are adding listeners, then you're passed the point of having the _middleware_ concept hidden from you, but I personally prefer a solution where they're a new field on the configureStore options (although looking at the implementation that might actually be quite difficult to do reliably).
The dispatch to add/remove is something we used at my workplace for another custom middleware (adding analytics handlers) and it worked really well for us. The only difference was that we cleansed the action of the attached function (so it was now serialisable) and passed it on. This way, other things could react to to the action type (although nothing ever _actually_ did) and it showed up in the dev tools, which was useful when debugging why we were reporting out analytics events multiple times (the "same" handler was being attached multiple times through multiple dispatches). All the reducers treated it as a noop so in the end there was very little downsides to passing a cleansed version of the action on. I'm not saying we should definitely do that this time, but it might be worth considering.
@mpeyper : roughly along those lines, yes.
Agreed that having to call getDefaultMiddleware({listeners: []}) is just a bit awkward, but I'm hesitant to add non-store-specific options to configureStore atm.
I've started a PR that implements the idea of action listeners (custom middleware) using the builder pattern: https://github.com/reduxjs/redux-toolkit/pull/432
Having spoken with @phryneas on #431, it would be great to see if #432 is a viable base for all the requirements in this thread (for which I will try to analyse soon).
This API would be pretty helpful for use cases where you have to create one-off subscribers for workflows. Here's an example: a button triggers an upload flow through a library that takes callbacks, which result in a "success" or "fail" action being dispatched. Then the thing that owns the button has to do something with the result (not handled by the reducer). You can detect the result in React.useEffect style by monitoring state changes and expressing conditions for the code to run in terms of state, but it seems a lot simpler to be able to subscribe to the event stream.
In some ways it seems like the base store object should just emit the event action + the new state to subscribers instead of just telling them to run getState() again (it is an Observable after all). Maybe there are batching, or purity, or perf reasons not to do that, but it could look like this:
```js
// store.js
const store = createStore(/* */)
// react-app.js
// show upload ui based on store state
// maybe-not-react.js
onUploadButtonClick = () => {
store.dispatch({ type: 'requestUpload' })
let observer = {};
observer.unsubscribe = store.subscribe((change) => {
if (change.action.type === 'uploadSuccess') {
this.setUrl(action.payload.url) // read from payload?
this.setUrl(selectUploadUrl(change.state)) // read from change object?
this.setUrl(selectUploadUrl(store.getState())) // read from store state?
observer.unsubscribe() // done listening
}
})
}
Lenz just put up a new PR at #547 that tries to implement this. After looking it through, I think it's pretty close what I was envisioning. Can we get some other eyes on that PR and some feedback?
Our app currently uses redux-saga. While incredibly powerful, it has a steep learning curve that can leave developers feeling confused/frustrated and it introduces unneeded complexity.
We've started looking into alternatives. Thunks get us close, but it's missing the event listener support we rely on in our sagas. This solution seems like a perfect blend of the two, and I was disappointed to see that any progress on this feature was dropped in May.
Is there anything I can do to help push this forward?
It feels like the big question atm is around what the desired semantics should be. That's where #547 bogged down. For example, see https://github.com/reduxjs/redux-toolkit/pull/547#issuecomment-628600873 and https://github.com/reduxjs/redux-toolkit/pull/547#issuecomment-632917147 .
There needs to be an agreement on what exact semantics this listener needs to implement, then figuring out how to implement those semantics and what the API should look like.
But seeing that the current state of #547 is quite usable, I'll probably just go and release that over into https://github.com/rtk-incubator sometime soon.
That way people can install it without relying on old codesandbox builds or having to copy-paste it out of a pull request and we can maybe get some real-life feedback on the API proposed there.
Most helpful comment
This is how I described this approach to my coworkers:
There are essentially two schools of thought for working with side effects with redux.
The first approach is to put the side effects in the actions -- e.g. instead of dispatching a simple value, dispatch a promise, or a function that calls side effects. Middleware like redux-promise and redux-thunk intercept these non-value actions and run them.
The second approach is to put the side effects directly into the middleware -- the actions are always simple values, but the middleware listens to these actions, and can run side effects in response to them. This is the approach used by redux-saga and redux-loop; it's also how logging and metrics-reporting middleware work.
I prefer the second approach because it encourages you to keep the "how" with the state, and the "what" with the UI. However, the tools associated with this approach, particularly redux-saga, tend to have a pretty steep learning curve. The approach I'm using here is less expressive, but much closer to the complexity of redux-thunk.