RFC: `withRedux`: global action dispatching / inter-store communication
Opened this issue · 3 comments
Update 1 (21.12 (17.01)): Self-dispatching external actions
Use Case
withRedux
integrates the Redux pattern into a signalStore. Actions are currently methods of the store instance which means we don't have a global dispatching mechanism.
Example:
const FlightStore = signalStore({
withEntities<Flight>(),
withRedux({
actions: {
search: payload<{from: string, to: string}>()
},
// ...
})
})
const BookingStore = signalStore({
withEntities<Booking>(),
withRedux({
actions: {
bookFlight: payload<{from: string, to: string}>()
},
// ...
})
})
It is not possible for bookingStore
to have a reducer on FlightStore::search
. It would be possible though to dispatch search
via an effect
or withMethods
:
const BookingStore = signalStore({
withEntities<Booking>(),
withRedux({
actions: {
bookFlight: payload<{from: string, to: string}>()
},
effect(actions, create) {
const flightStore = inject(FlightStore);
return {
bookFlight$: create(actions.bookFlight).pipe(tap(() => flightStore.search({from: 'London', to: 'Vienna'})))
}
}
})
})
That works as long as we have only global stores. As soon as a global store, wants to listen to actions from al local one, the DI will fail.
Proposed Approach:
Here's a design which would introduce global actions for global and local SignalStores.
We require two new features:
- Global (self-dispatching) Actions
- reducer option to consume "instance-only" dispatched actions or global ones.
To reference actions without a store's instance, we need to be able to externalize them. That is exactly what we have with the Global Store:
export const flightActions = createActions('flights', {search: payload<{from: string, to: string}>()})
const FlightStore = signalStore({
withEntities<Flight>(),
withRedux({
actions: flightActions
// ...
})
})
// ...
If another Signal Store wants to react to that action, it can do:
withRedux({
reducer(, on) {
on(flightActions.search, (state, {from, to}) => patchState(state, {loading: true}));
},
// ...
})
It will still be possible to define actions inside withRedux::actions
.
External actions are self-dispatching. They do not require a "DispatcherService", like the Store
in the Global Store:
withRedux({
effect(actions, create) {
return {
bookFlight$: create(actions.bookFlight).pipe(tap(() => flightActions.search({from: 'London', to: 'Vienna'})))
}
}
})
In contrast to the global store, Signal Stores can be provided multiple times. That means we have more than one instance.
There are use cases, where a "local Signal Store" only needs to consume actions dispatched by its instance. The on
method will get an optional option to
const FlightStore = signalStore({
withEntities<Flight>(),
withRedux({
actions: {
searched: payload<{flights: Flight[]}>()
},
reducer(actions, on) {
on(actions.searched, (state, {flights}) => patchState(state, {flights}), {globalActions: false})
}
// ...
})
})
Actions have to be unique per Store class.
Abandoned (simpler) Approach: storeBound actions & inject in reducer
const BookingStore = signalStore({
withEntities<Booking>(),
withRedux({
actions: {
bookFlight: payload<{from: string, to: string}>()
},
reducer(actions, on) {
const filghtStore = inject(FlightStore);
on(flightStore.search, (state) => patchState(state, {loading: true}));
},
effect(actions, create) {
const flightStore = inject(FlightStore);
return {
bookFlight$: create(actions.bookFlight).pipe(tap(() => flightStore.search({from: 'London', to: 'Vienna'})))
}
}
})
})
I prefer this approach and have a question: if a store has multiple instances, it means you need to add the dispatcher to the instance provider, right? How would you do that?
For compontent provided stores, it would look like this:
const FlightStore = signalStore({
withEntities<Flight>(),
withRedux({
actions: {
searched: payload<{flights: Flight[]}>()
},
reducer(actions, on) {
on(actions.searched, (state, {flights}) => patchState(state, {flights}), {globalActions: false})
}
// ...
})
})
const flightStore = new FlightStore();
flightStore.searched({flights: []});
So you would have to have access to the store instance in order to dispatch a local-managed action.
Leaving my suggestion here: https://x.com/MarkoStDev/status/1753556633089704059?s=20