Skip to content

Commit 2df8375

Browse files
committed
feat(devtools): expose tracked event payloads from withTrackedReducer
withTrackedReducer now forwards full event objects to updateState so Redux DevTools can show payload data, not only action names.
1 parent 6625d07 commit 2df8375

9 files changed

Lines changed: 88 additions & 11 deletions

File tree

apps/demo/src/app/feature-factory/feature-factory.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function withMyEntity<Entity>(loadMethod: (id: number) => Promise<Entity>) {
3434
const UserStore = signalStore(
3535
{ providedIn: 'root' },
3636
withMethods(() => ({
37-
findById(id: number) {
37+
findById(_id: number) {
3838
return of({ id: 1, name: 'Konrad' });
3939
},
4040
})),

docs/docs/with-devtools.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The extensions don't activate during app initialization (as it is with `@ngrx/st
4141
import { updateState } from '@angular-architects/ngrx-toolkit';
4242
```
4343

44-
The Signal Store does not use the Redux pattern, so there are no action names involved by default. Instead, every action is referred to as a "Store Update". If you want to customize the action name for better clarity, you can use the `updateState()` function instead of `patchState()`:
44+
The Signal Store does not use the Redux pattern, so there are no action names involved by default. Instead, every action is referred to as a "Store Update". If you want to customize the action entry for better clarity, you can use the `updateState()` function instead of `patchState()`:
4545

4646
```typescript
4747
import { updateState } from '@angular-architects/ngrx-toolkit';
@@ -50,6 +50,9 @@ patchState(this.store, { loading: false });
5050

5151
// updateState is a wrapper around patchState and has an action name as second parameter
5252
updateState(this.store, 'update loading', { loading: false });
53+
54+
// updateState also accepts an action object with a mandatory type field
55+
updateState(this.store, { type: '[Book Store] bookSelected', payload: { bookId: '1' } }, { selectedBookId: '1' });
5356
```
5457

5558
## `renameDevtoolsName()`
@@ -172,7 +175,7 @@ const Store = signalStore(
172175
## Events tracking: `withTrackedReducer`
173176

174177
`withTrackedReducer` tracks state changes within the events
175-
plugin. This utility automatically derives the event name, streamlining
178+
plugin. This utility automatically derives the event entry, streamlining
176179
the tracking process.
177180

178181
To use it
@@ -188,6 +191,7 @@ export const bookEvents = eventGroup({
188191
source: 'Book Store',
189192
events: {
190193
loadBooks: type<void>(),
194+
bookSelected: type<{ bookId: string }>(),
191195
},
192196
});
193197

@@ -202,6 +206,10 @@ const Store = signalStore(
202206
on(bookEvents.loadBooks, () => ({
203207
books: mockBooks,
204208
})),
209+
// DevTools action will include payload: { bookId: string }
210+
on(bookEvents.bookSelected, ({ payload }) => ({
211+
selectedBookId: payload.bookId,
212+
})),
205213
),
206214
withHooks({
207215
onInit() {
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export const currentActionNames = new Set<string>();
1+
import { Action } from './models';
2+
3+
export const currentActionNames = new Set<string | Action>();

libs/ngrx-toolkit/src/lib/devtools/internal/devtools-syncer.service.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,25 @@ import { StateSource } from '@ngrx/signals';
1010
import { REDUX_DEVTOOLS_CONFIG } from '../provide-devtools-config';
1111
import { currentActionNames } from './current-action-names';
1212
import { DevtoolsInnerOptions } from './devtools-feature';
13-
import { Connection, StoreRegistry, Tracker } from './models';
13+
import { Action, Connection, StoreRegistry, Tracker } from './models';
1414

1515
const dummyConnection: Connection = {
1616
send: () => void true,
1717
};
1818

19+
function toDevtoolsAction(actions: (string | Action)[]): Action {
20+
if (!actions.length) {
21+
return { type: 'Store Update' };
22+
}
23+
24+
const objects = actions.filter((a): a is Action => typeof a === 'object');
25+
const type = [
26+
...new Set(actions.map((a) => (typeof a === 'string' ? a : a.type))),
27+
].join(', ');
28+
29+
return objects.length ? { ...objects[0], type } : { type };
30+
}
31+
1932
/**
2033
* A service provided by the root injector is
2134
* required because the synchronization runs
@@ -90,11 +103,11 @@ export class DevtoolsSyncer implements OnDestroy {
90103
...mappedChangedStatePerName,
91104
};
92105

93-
const names = Array.from(currentActionNames);
94-
const type = names.length ? names.join(', ') : 'Store Update';
106+
const actions = Array.from(currentActionNames);
107+
const action = toDevtoolsAction(actions);
95108
currentActionNames.clear();
96109

97-
this.#connection.send({ type }, this.#currentState);
110+
this.#connection.send(action, this.#currentState);
98111
}
99112

100113
getNextId() {

libs/ngrx-toolkit/src/lib/devtools/internal/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { StateSource } from '@ngrx/signals';
22
import { ReduxDevtoolsConfig } from '../provide-devtools-config';
33
import { DevtoolsInnerOptions } from './devtools-feature';
44

5-
export type Action = { type: string };
5+
export type Action = { type: string; [key: string]: unknown };
66
export type Connection = {
77
send: (action: Action, state: Record<string, unknown>) => void;
88
};

libs/ngrx-toolkit/src/lib/devtools/tests/action-name.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,29 @@ describe('updateState', () => {
4545
{ shop: { name: 'i4' } },
4646
);
4747
});
48+
49+
it('should set and send an action object', () => {
50+
const { sendSpy } = setupExtensions();
51+
52+
const Store = signalStore(
53+
{ providedIn: 'root' },
54+
withDevtools('shop'),
55+
withState({ name: 'Car' }),
56+
withMethods((store) => ({
57+
setName(name: string) {
58+
updateState(store, { type: 'Set Name', name }, { name });
59+
},
60+
})),
61+
);
62+
const store = TestBed.inject(Store);
63+
TestBed.flushEffects();
64+
65+
store.setName('i4');
66+
TestBed.flushEffects();
67+
68+
expect(sendSpy).toHaveBeenLastCalledWith(
69+
{ type: 'Set Name', name: 'i4' },
70+
{ shop: { name: 'i4' } },
71+
);
72+
});
4873
});

libs/ngrx-toolkit/src/lib/devtools/tests/with-tracked-reducer.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const testEvents = eventGroup({
2828
source: 'Spec Store',
2929
events: {
3030
bump: type<void>(),
31+
bookSelected: type<{ bookId: string }>(),
3132
},
3233
});
3334

@@ -79,6 +80,29 @@ describe('withTrackedReducer', () => {
7980
);
8081
});
8182

83+
it('should send event payload to devtools when using tracked reducer', () => {
84+
const { sendSpy, withBasicStore } = setup();
85+
86+
const Store = signalStore(
87+
{ providedIn: 'root' },
88+
withBasicStore('store'),
89+
withTrackedReducer(
90+
on(testEvents.bookSelected, ({ payload }) => ({
91+
count: Number(payload.bookId),
92+
})),
93+
),
94+
);
95+
96+
TestBed.inject(Store);
97+
98+
dispatchBookSelectedEvent('42');
99+
100+
expect(sendSpy).toHaveBeenLastCalledWith(
101+
{ type: '[Spec Store] bookSelected', payload: { bookId: '42' } },
102+
{ store: { count: 42 } },
103+
);
104+
});
105+
82106
it('should distinguish between two synchronous state changes in reducer and normal patchState', () => {
83107
const { sendSpy, withBasicStore } = setup();
84108

@@ -260,6 +284,10 @@ function dispatchBumpEvent() {
260284
TestBed.inject(Dispatcher).dispatch(testEvents.bump());
261285
}
262286

287+
function dispatchBookSelectedEvent(bookId: string) {
288+
TestBed.inject(Dispatcher).dispatch(testEvents.bookSelected({ bookId }));
289+
}
290+
263291
function setup() {
264292
const { sendSpy } = setupExtensions();
265293

libs/ngrx-toolkit/src/lib/devtools/update-state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
WritableStateSource,
55
} from '@ngrx/signals';
66
import { currentActionNames } from './internal/current-action-names';
7+
import { Action } from './internal/models';
78

89
type PatchFn = typeof originalPatchState extends (
910
arg1: infer First,
@@ -28,7 +29,7 @@ export const patchState: PatchFn = (state, action, ...rest) => {
2829
*/
2930
export function updateState<State extends object>(
3031
stateSource: WritableStateSource<State>,
31-
action: string,
32+
action: string | Action,
3233
...updaters: Array<
3334
Partial<NoInfer<State>> | PartialStateUpdater<NoInfer<State>>
3435
>

libs/ngrx-toolkit/src/lib/devtools/with-tracked-reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function withTrackedReducer<State extends object>(
3939
const result = caseReducer.reducer(event, state);
4040
const updaters = Array.isArray(result) ? result : [result];
4141

42-
updateState(store, event.type, ...updaters);
42+
updateState(store, event, ...updaters);
4343
}),
4444
),
4545
),

0 commit comments

Comments
 (0)