Skip to content

Commit ef7ba1f

Browse files
feat(redux): add possibility to extract elements to separate files
`createReducer` and `createEffects` an extraction into separate files. actions can also be separated, but there is no need for an own `createActions` function, because it is a simple object literal.
1 parent d4913a7 commit ef7ba1f

File tree

5 files changed

+248
-12
lines changed

5 files changed

+248
-12
lines changed

apps/demo/src/app/flight-search/flight-store.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,22 @@ import { HttpClient, HttpParams } from '@angular/common/http';
1111
import { map, switchMap } from 'rxjs';
1212
import { Flight } from './flight';
1313

14+
const actions = {
15+
public: {
16+
loadFlights: payload<{ from: string; to: string }>(),
17+
delayFirst: noPayload,
18+
},
19+
private: {
20+
flightsLoaded: payload<{ flights: Flight[] }>(),
21+
},
22+
};
23+
1424
export const FlightStore = signalStore(
1525
{ providedIn: 'root' },
1626
withDevtools('flights'),
1727
withState({ flights: [] as Flight[] }),
1828
withRedux({
19-
actions: {
20-
public: {
21-
loadFlights: payload<{ from: string; to: string }>(),
22-
delayFirst: noPayload,
23-
},
24-
private: {
25-
flightsLoaded: payload<{ flights: Flight[] }>(),
26-
},
27-
},
28-
29+
actions,
2930
reducer: (actions, on) => {
3031
on(actions.flightsLoaded, (state, { flights }) => {
3132
updateState(state, 'flights loaded', { flights });

docs/docs/with-redux.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,60 @@ export const FlightStore = signalStore(
5050
})
5151
);
5252
```
53+
54+
## Extracting actions, reducer and effects into separate files
55+
56+
`createReducer` and `createEffects` allow you to extract the reducer and effects into separate files.
57+
58+
There is no need for a `createActions` function, because the actions are just an object literal.
59+
60+
Example:
61+
62+
```typescript
63+
interface FlightState {
64+
flights: Flight[];
65+
effect1: boolean;
66+
effect2: boolean;
67+
}
68+
69+
const initialState: FlightState = {
70+
flights: [],
71+
effect1: false,
72+
effect2: false,
73+
};
74+
75+
// this can be in a separate file
76+
const actions = {
77+
init: noPayload,
78+
updateEffect1: payload<{ value: boolean }>(),
79+
updateEffect2: payload<{ value: boolean }>(),
80+
};
81+
82+
// this can be in a separate file
83+
const reducer = createReducer<FlightState, typeof actions>((actions, on) => {
84+
on(actions.updateEffect1, (state, { value }) => {
85+
patchState(state, { effect1: value });
86+
});
87+
88+
on(actions.updateEffect2, (state, { value }) => {
89+
patchState(state, { effect2: value });
90+
});
91+
});
92+
93+
// this can be in a separate file
94+
const effects = createEffects(actions, (actions, create) => {
95+
return {
96+
init1$: create(actions.init).pipe(map(() => actions.updateEffect1({ value: true }))),
97+
init2$: create(actions.init).pipe(map(() => actions.updateEffect2({ value: true }))),
98+
};
99+
});
100+
101+
signalStore(
102+
withState(initialState),
103+
withRedux({
104+
actions,
105+
effects,
106+
reducer,
107+
})
108+
);
109+
```

libs/ngrx-toolkit/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ export { withGlitchTracking } from './lib/devtools/features/with-glitch-tracking
66
export { patchState, updateState } from './lib/devtools/update-state';
77
export { renameDevtoolsName } from './lib/devtools/rename-devtools-name';
88

9-
export { withRedux, payload, noPayload } from './lib/with-redux';
9+
export {
10+
withRedux,
11+
payload,
12+
noPayload,
13+
createReducer,
14+
createEffects,
15+
} from './lib/with-redux';
1016

1117
export * from './lib/with-call-state';
1218
export * from './lib/with-undo-redo';

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
provideHttpClient,
77
} from '@angular/common/http';
88
import { map, switchMap } from 'rxjs';
9-
import { noPayload, payload, withRedux } from './with-redux';
9+
import {
10+
createEffects,
11+
createReducer,
12+
noPayload,
13+
payload,
14+
withRedux,
15+
} from './with-redux';
1016
import { TestBed } from '@angular/core/testing';
1117
import {
1218
HttpTestingController,
@@ -161,4 +167,65 @@ describe('with redux', () => {
161167
expect(flightStore.effect1()).toBe(true);
162168
expect(flightStore.effect2()).toBe(true);
163169
});
170+
171+
it('should be possible to separate actions, reducer and effects', () => {
172+
interface FlightState {
173+
flights: Flight[];
174+
effect1: boolean;
175+
effect2: boolean;
176+
}
177+
178+
const initialState: FlightState = {
179+
flights: [],
180+
effect1: false,
181+
effect2: false,
182+
};
183+
184+
const actions = {
185+
init: noPayload,
186+
updateEffect1: payload<{ value: boolean }>(),
187+
updateEffect2: payload<{ value: boolean }>(),
188+
};
189+
190+
const effects = createEffects(actions, (actions, create) => {
191+
return {
192+
init1$: create(actions.init).pipe(
193+
map(() => actions.updateEffect1({ value: true }))
194+
),
195+
init2$: create(actions.init).pipe(
196+
map(() => actions.updateEffect2({ value: true }))
197+
),
198+
};
199+
});
200+
201+
const reducer = createReducer<FlightState, typeof actions>(
202+
(actions, on) => {
203+
on(actions.updateEffect1, (state, { value }) => {
204+
patchState(state, { effect1: value });
205+
});
206+
207+
on(actions.updateEffect2, (state, { value }) => {
208+
patchState(state, { effect2: value });
209+
});
210+
}
211+
);
212+
213+
const FlightsStore = signalStore(
214+
withState(initialState),
215+
withRedux({
216+
actions,
217+
effects,
218+
reducer,
219+
})
220+
);
221+
222+
const flightStore = TestBed.configureTestingModule({
223+
providers: [FlightsStore],
224+
}).inject(FlightsStore);
225+
226+
flightStore.init();
227+
228+
expect(flightStore.effect1()).toBe(true);
229+
expect(flightStore.effect2()).toBe(true);
230+
});
164231
});

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,60 @@ type ReducerFactory<StateActionFns extends ActionFns, State> = (
7070
) => void
7171
) => void;
7272

73+
/**
74+
* Creates a reducer function to separate the reducer logic into another file.
75+
*
76+
* ```typescript
77+
* interface FlightState {
78+
* flights: Flight[];
79+
* effect1: boolean;
80+
* effect2: boolean;
81+
* }
82+
*
83+
* const initialState: FlightState = {
84+
* flights: [],
85+
* effect1: false,
86+
* effect2: false,
87+
* };
88+
*
89+
* const actions = {
90+
* init: noPayload,
91+
* updateEffect1: payload<{ value: boolean }>(),
92+
* updateEffect2: payload<{ value: boolean }>(),
93+
* };
94+
*
95+
* const reducer = createReducer<FlightState, typeof actions>((actions, on) => {
96+
* on(actions.updateEffect1, (state, { value }) => {
97+
* patchState(state, { effect1: value });
98+
* });
99+
*
100+
* on(actions.updateEffect2, (state, { value }) => {
101+
* patchState(state, { effect2: value });
102+
* });
103+
* });
104+
*
105+
* signalStore(
106+
* withState(initialState),
107+
* withRedux({
108+
* actions,
109+
* reducer,
110+
* })
111+
* );
112+
* ```
113+
* @param reducerFactory
114+
*/
115+
export function createReducer<
116+
State extends object,
117+
Actions extends ActionsFnSpecs
118+
>(
119+
reducerFactory: ReducerFactory<
120+
ActionFnsCreator<Actions>,
121+
WritableStateSource<State>
122+
>
123+
) {
124+
return reducerFactory;
125+
}
126+
73127
/** Effect **/
74128

75129
type EffectsFactory<StateActionFns extends ActionFns> = (
@@ -79,6 +133,57 @@ type EffectsFactory<StateActionFns extends ActionFns> = (
79133
) => Observable<ActionFnPayload<EffectAction>>
80134
) => Record<string, Observable<unknown>>;
81135

136+
/**
137+
* Creates the effects function to separate the effects logic into another file.
138+
*
139+
* ```typescript
140+
* interface FlightState {
141+
* flights: Flight[];
142+
* effect1: boolean;
143+
* effect2: boolean;
144+
* }
145+
*
146+
* const initialState: FlightState = {
147+
* flights: [],
148+
* effect1: false,
149+
* effect2: false,
150+
* };
151+
*
152+
* const actions = {
153+
* init: noPayload,
154+
* updateEffect1: payload<{ value: boolean }>(),
155+
* updateEffect2: payload<{ value: boolean }>(),
156+
* };
157+
*
158+
* const effects = createEffects(actions, (actions, create) => {
159+
* return {
160+
* init1$: create(actions.init).pipe(
161+
* map(() => actions.updateEffect1({ value: true }))
162+
* ),
163+
* init2$: create(actions.init).pipe(
164+
* map(() => actions.updateEffect2({ value: true }))
165+
* ),
166+
* };
167+
* });
168+
*
169+
* signalStore(
170+
* withState(initialState),
171+
* withRedux({
172+
* actions,
173+
* effects,
174+
* })
175+
* );
176+
* ```
177+
* @param actions
178+
* @param effectsFactory
179+
*/
180+
export function createEffects<Actions extends ActionsFnSpecs>(
181+
actions: Actions,
182+
effectsFactory: EffectsFactory<ActionFnsCreator<Actions>>
183+
) {
184+
return effectsFactory;
185+
}
186+
82187
// internal types
83188

84189
/**

0 commit comments

Comments
 (0)