Skip to content

Commit 916fba0

Browse files
feat(signals): add withHooks signature with factory input (#4208)
Closes #4201
1 parent 3eb7cf3 commit 916fba0

File tree

2 files changed

+78
-23
lines changed

2 files changed

+78
-23
lines changed

modules/signals/spec/signal-store.spec.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,32 +268,58 @@ describe('signalStore', () => {
268268
expect(message).toBe('onDestroy');
269269
});
270270

271-
// FIX: injection context will be provided for `onDestroy` in a separate PR
272-
// see https://github.com/ngrx/platform/pull/4196#issuecomment-1875228588
273-
it('executes hooks in injection context', () => {
271+
it('executes hooks factory in injection context', () => {
274272
const messages: string[] = [];
275-
const TOKEN = new InjectionToken('TOKEN', {
273+
const TOKEN_INIT = new InjectionToken('TOKEN_INIT', {
274+
providedIn: 'root',
275+
factory: () => 'init',
276+
});
277+
const TOKEN_DESTROY = new InjectionToken('TOKEN_DESTROY', {
276278
providedIn: 'root',
277-
factory: () => 'ngrx',
279+
factory: () => 'destroy',
278280
});
279281
const Store = signalStore(
282+
withState({ name: 'NgRx Store' }),
283+
withHooks((store) => {
284+
const tokenInit = inject(TOKEN_INIT);
285+
const tokenDestroy = inject(TOKEN_DESTROY);
286+
return {
287+
onInit() {
288+
messages.push(`${tokenInit} ${store.name()}`);
289+
},
290+
onDestroy() {
291+
messages.push(`${tokenDestroy} ${store.name()}`);
292+
},
293+
};
294+
})
295+
);
296+
const { destroy } = createLocalService(Store);
297+
298+
expect(messages).toEqual(['init NgRx Store']);
299+
300+
destroy();
301+
expect(messages).toEqual(['init NgRx Store', 'destroy NgRx Store']);
302+
});
303+
304+
it('executes hooks without injection context', () => {
305+
const messages: string[] = [];
306+
const Store = signalStore(
307+
withState({ name: 'NgRx Store' }),
280308
withHooks({
281-
onInit() {
282-
inject(TOKEN);
283-
messages.push('onInit');
309+
onInit(store) {
310+
messages.push(`init ${store.name()}`);
284311
},
285-
onDestroy() {
286-
// inject(TOKEN);
287-
messages.push('onDestroy');
312+
onDestroy(store) {
313+
messages.push(`destroy ${store.name()}`);
288314
},
289315
})
290316
);
291317
const { destroy } = createLocalService(Store);
292318

293-
expect(messages).toEqual(['onInit']);
319+
expect(messages).toEqual(['init NgRx Store']);
294320

295321
destroy();
296-
expect(messages).toEqual(['onInit', 'onDestroy']);
322+
expect(messages).toEqual(['init NgRx Store', 'destroy NgRx Store']);
297323
});
298324

299325
it('succeeds with onDestroy and providedIn: root', () => {

modules/signals/src/with-hooks.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from './signal-store-models';
88
import { Prettify } from './ts-helpers';
99

10-
type HooksFactory<Input extends SignalStoreFeatureResult> = (
10+
type HookFn<Input extends SignalStoreFeatureResult> = (
1111
store: Prettify<
1212
SignalStoreSlices<Input['state']> &
1313
Input['signals'] &
@@ -16,11 +16,45 @@ type HooksFactory<Input extends SignalStoreFeatureResult> = (
1616
>
1717
) => void;
1818

19+
type HooksFactory<Input extends SignalStoreFeatureResult> = (
20+
store: Prettify<
21+
SignalStoreSlices<Input['state']> &
22+
Input['signals'] &
23+
Input['methods'] &
24+
StateSignal<Prettify<Input['state']>>
25+
>
26+
) => {
27+
onInit?: () => void;
28+
onDestroy?: () => void;
29+
};
30+
1931
export function withHooks<Input extends SignalStoreFeatureResult>(hooks: {
20-
onInit?: HooksFactory<Input>;
21-
onDestroy?: HooksFactory<Input>;
22-
}): SignalStoreFeature<Input, EmptyFeatureResult> {
32+
onInit?: HookFn<Input>;
33+
onDestroy?: HookFn<Input>;
34+
}): SignalStoreFeature<Input, EmptyFeatureResult>;
35+
export function withHooks<Input extends SignalStoreFeatureResult>(
36+
hooks: HooksFactory<Input>
37+
): SignalStoreFeature<Input, EmptyFeatureResult>;
38+
39+
export function withHooks<Input extends SignalStoreFeatureResult>(
40+
hooksOrFactory:
41+
| {
42+
onInit?: HookFn<Input>;
43+
onDestroy?: HookFn<Input>;
44+
}
45+
| HooksFactory<Input>
46+
): SignalStoreFeature<Input, EmptyFeatureResult> {
2347
return (store) => {
48+
const storeProps = {
49+
[STATE_SIGNAL]: store[STATE_SIGNAL],
50+
...store.slices,
51+
...store.signals,
52+
...store.methods,
53+
};
54+
const hooks =
55+
typeof hooksOrFactory === 'function'
56+
? hooksOrFactory(storeProps)
57+
: hooksOrFactory;
2458
const createHook = (name: keyof typeof hooks) => {
2559
const hook = hooks[name];
2660
const currentHook = store.hooks[name];
@@ -31,12 +65,7 @@ export function withHooks<Input extends SignalStoreFeatureResult>(hooks: {
3165
currentHook();
3266
}
3367

34-
hook({
35-
[STATE_SIGNAL]: store[STATE_SIGNAL],
36-
...store.slices,
37-
...store.signals,
38-
...store.methods,
39-
});
68+
hook(storeProps);
4069
}
4170
: currentHook;
4271
};

0 commit comments

Comments
 (0)