Skip to content

Commit 12d0077

Browse files
fix(redux): multiple effects can subscribe to the same action
1 parent 92a47ef commit 12d0077

File tree

2 files changed

+84
-27
lines changed

2 files changed

+84
-27
lines changed

libs/ngrx-toolkit/src/lib/with-redux.spec.ts

+42
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
HttpTestingController,
1313
provideHttpClientTesting,
1414
} from '@angular/common/http/testing';
15+
import { Action } from 'ngrx-toolkit';
1516

1617
interface Flight {
1718
id: number;
@@ -97,4 +98,45 @@ describe('with redux', () => {
9798
controller.verify();
9899
});
99100
});
101+
102+
it('should allow multiple effects listening to the same action', () => {
103+
const FlightsStore = signalStore(
104+
withState({ flights: [] as Flight[], effect1: false, effect2: false }),
105+
withRedux({
106+
actions: {
107+
init: noPayload,
108+
updateEffect1: payload<{ value: boolean }>(),
109+
updateEffect2: payload<{ value: boolean }>(),
110+
},
111+
reducer(actions, on) {
112+
on(actions.updateEffect1, (state, { value }) => {
113+
patchState(state, { effect1: value });
114+
});
115+
116+
on(actions.updateEffect2, (state, { value }) => {
117+
patchState(state, { effect2: value });
118+
});
119+
},
120+
effects(actions, create) {
121+
return {
122+
init1$: create(actions.init).pipe(
123+
map(() => actions.updateEffect1({ value: true })),
124+
),
125+
init2$: create(actions.init).pipe(
126+
map(() => actions.updateEffect2({ value: true })),
127+
),
128+
};
129+
},
130+
}),
131+
);
132+
133+
const flightStore = TestBed.configureTestingModule({
134+
providers: [FlightsStore],
135+
}).inject(FlightsStore);
136+
137+
flightStore.init({});
138+
139+
expect(flightStore.effect1()).toBe(true);
140+
expect(flightStore.effect2()).toBe(true);
141+
});
100142
});

libs/ngrx-toolkit/src/lib/with-redux.ts

+42-27
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type Payload = Record<string, unknown>;
1313

1414
type ActionFn<
1515
Type extends string = string,
16-
ActionPayload extends Payload = Payload
16+
ActionPayload extends Payload = Payload,
1717
> = ((payload: ActionPayload) => ActionPayload & { type: Type }) & {
1818
type: Type;
1919
};
@@ -24,7 +24,7 @@ export type ActionsFnSpecs = Record<string, Payload>;
2424

2525
type ActionFnCreator<Spec extends ActionsFnSpecs> = {
2626
[ActionName in keyof Spec]: ((
27-
payload: Spec[ActionName]
27+
payload: Spec[ActionName],
2828
) => Spec[ActionName] & { type: ActionName }) & { type: ActionName & string };
2929
};
3030

@@ -55,34 +55,43 @@ export const noPayload = {};
5555

5656
type ReducerFunction<ReducerAction, State> = (
5757
state: State,
58-
action: ActionFnPayload<ReducerAction>
58+
action: ActionFnPayload<ReducerAction>,
5959
) => void;
6060

6161
type ReducerFactory<StateActionFns extends ActionFns, State> = (
6262
actions: StateActionFns,
6363
on: <ReducerAction extends { type: string }>(
6464
action: ReducerAction,
65-
reducerFn: ReducerFunction<ReducerAction, State>
66-
) => void
65+
reducerFn: ReducerFunction<ReducerAction, State>,
66+
) => void,
6767
) => void;
6868

6969
/** Effect **/
7070

7171
type EffectsFactory<StateActionFns extends ActionFns> = (
7272
actions: StateActionFns,
7373
create: <EffectAction extends { type: string }>(
74-
action: EffectAction
75-
) => Observable<ActionFnPayload<EffectAction>>
74+
action: EffectAction,
75+
) => Observable<ActionFnPayload<EffectAction>>,
7676
) => Record<string, Observable<unknown>>;
7777

78+
// internal types
79+
80+
/**
81+
* Record which holds all effects for a specific action type.
82+
* The values are Subject which the effect are subscribed to.
83+
* `createActionFns` will call next on these subjects.
84+
*/
85+
type EffectsRegistry = Record<string, Subject<ActionFnPayload<unknown>>[]>;
86+
7887
function createActionFns<Spec extends ActionsFnSpecs>(
7988
actionFnSpecs: Spec,
8089
reducerRegistry: Record<
8190
string,
8291
(state: unknown, payload: ActionFnPayload<unknown>) => void
8392
>,
84-
effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>>,
85-
state: unknown
93+
effectsRegistry: EffectsRegistry,
94+
state: unknown,
8695
) {
8796
const actionFns: Record<string, ActionFn> = {};
8897

@@ -93,12 +102,14 @@ function createActionFns<Spec extends ActionsFnSpecs>(
93102
if (reducer) {
94103
(reducer as (state: unknown, payload: unknown) => void)(
95104
state,
96-
fullPayload as unknown
105+
fullPayload as unknown,
97106
);
98107
}
99-
const effectSubject = effectsRegistry[type];
100-
if (effectSubject) {
101-
(effectSubject as unknown as Subject<unknown>).next(fullPayload);
108+
const effectSubjects = effectsRegistry[type];
109+
if (effectSubjects?.length) {
110+
for (const effectSubject of effectSubjects) {
111+
(effectSubject as unknown as Subject<unknown>).next(fullPayload);
112+
}
102113
}
103114
return fullPayload;
104115
};
@@ -115,8 +126,8 @@ function createPublicAndAllActionsFns<Spec extends ActionsFnSpecs>(
115126
string,
116127
(state: unknown, payload: ActionFnPayload<unknown>) => void
117128
>,
118-
effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>>,
119-
state: unknown
129+
effectsRegistry: EffectsRegistry,
130+
state: unknown,
120131
): { all: ActionFns; publics: ActionFns } {
121132
if ('public' in actionFnSpecs || 'private' in actionFnSpecs) {
122133
const privates = actionFnSpecs['private'] || {};
@@ -129,13 +140,13 @@ function createPublicAndAllActionsFns<Spec extends ActionsFnSpecs>(
129140
privates,
130141
reducerRegistry,
131142
effectsRegistry,
132-
state
143+
state,
133144
);
134145
const publicActionFns = createActionFns(
135146
publics,
136147
reducerRegistry,
137148
effectsRegistry,
138-
state
149+
state,
139150
);
140151

141152
return {
@@ -148,7 +159,7 @@ function createPublicAndAllActionsFns<Spec extends ActionsFnSpecs>(
148159
actionFnSpecs,
149160
reducerRegistry,
150161
effectsRegistry,
151-
state
162+
state,
152163
);
153164

154165
return { all: actionFns, publics: actionFns };
@@ -160,11 +171,11 @@ function fillReducerRegistry(
160171
reducerRegistry: Record<
161172
string,
162173
(state: unknown, payload: ActionFnPayload<unknown>) => void
163-
>
174+
>,
164175
) {
165176
function on(
166177
action: { type: string },
167-
reducerFn: (state: unknown, payload: ActionFnPayload<unknown>) => void
178+
reducerFn: (state: unknown, payload: ActionFnPayload<unknown>) => void,
168179
) {
169180
reducerRegistry[action.type] = reducerFn;
170181
}
@@ -177,11 +188,14 @@ function fillReducerRegistry(
177188
function fillEffects(
178189
effects: EffectsFactory<ActionFns>,
179190
actionFns: ActionFns,
180-
effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>> = {}
191+
effectsRegistry: EffectsRegistry = {},
181192
): Observable<unknown>[] {
182193
function create(action: { type: string }) {
183194
const subject = new Subject<ActionFnPayload<unknown>>();
184-
effectsRegistry[action.type] = subject;
195+
if (!(action.type in effectsRegistry)) {
196+
effectsRegistry[action.type] = [];
197+
}
198+
effectsRegistry[action.type].push(subject);
185199
return subject.asObservable();
186200
}
187201

@@ -197,18 +211,19 @@ function processRedux<Spec extends ActionsFnSpecs, ReturnType>(
197211
actionFnSpecs: Spec,
198212
reducer: ReducerFactory<ActionFns, unknown>,
199213
effects: EffectsFactory<ActionFns>,
200-
store: unknown
214+
store: unknown,
201215
) {
202216
const reducerRegistry: Record<
203217
string,
204218
(state: unknown, payload: ActionFnPayload<unknown>) => void
205219
> = {};
206-
const effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>> = {};
220+
const effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>[]> =
221+
{};
207222
const actionsMap = createPublicAndAllActionsFns(
208223
actionFnSpecs,
209224
reducerRegistry,
210225
effectsRegistry,
211-
store
226+
store,
212227
);
213228
const actionFns = actionsMap.all;
214229
const publicActionsFns = actionsMap.publics;
@@ -237,7 +252,7 @@ export function withRedux<
237252
Spec extends ActionsFnSpecs,
238253
Input extends SignalStoreFeatureResult,
239254
StateActionFns extends ActionFnsCreator<Spec> = ActionFnsCreator<Spec>,
240-
PublicStoreActionFns extends PublicActionFns<Spec> = PublicActionFns<Spec>
255+
PublicStoreActionFns extends PublicActionFns<Spec> = PublicActionFns<Spec>,
241256
>(redux: {
242257
actions: Spec;
243258
reducer: ReducerFactory<StateActionFns, StateSignal<Input['state']>>;
@@ -251,7 +266,7 @@ export function withRedux<
251266
redux.actions,
252267
redux.reducer as ReducerFactory<ActionFns, unknown>,
253268
redux.effects as EffectsFactory<ActionFns>,
254-
store
269+
store,
255270
);
256271
return {
257272
...store,

0 commit comments

Comments
 (0)