diff --git a/modules/signals/spec/signal-store.spec.ts b/modules/signals/spec/signal-store.spec.ts index ce2a618cb6..2b73050fde 100644 --- a/modules/signals/spec/signal-store.spec.ts +++ b/modules/signals/spec/signal-store.spec.ts @@ -268,32 +268,58 @@ describe('signalStore', () => { expect(message).toBe('onDestroy'); }); - // FIX: injection context will be provided for `onDestroy` in a separate PR - // see https://github.com/ngrx/platform/pull/4196#issuecomment-1875228588 - it('executes hooks in injection context', () => { + it('executes hooks factory in injection context', () => { const messages: string[] = []; - const TOKEN = new InjectionToken('TOKEN', { + const TOKEN_INIT = new InjectionToken('TOKEN_INIT', { + providedIn: 'root', + factory: () => 'init', + }); + const TOKEN_DESTROY = new InjectionToken('TOKEN_DESTROY', { providedIn: 'root', - factory: () => 'ngrx', + factory: () => 'destroy', }); const Store = signalStore( + withState({ name: 'NgRx Store' }), + withHooks((store) => { + const tokenInit = inject(TOKEN_INIT); + const tokenDestroy = inject(TOKEN_DESTROY); + return { + onInit() { + messages.push(`${tokenInit} ${store.name()}`); + }, + onDestroy() { + messages.push(`${tokenDestroy} ${store.name()}`); + }, + }; + }) + ); + const { destroy } = createLocalService(Store); + + expect(messages).toEqual(['init NgRx Store']); + + destroy(); + expect(messages).toEqual(['init NgRx Store', 'destroy NgRx Store']); + }); + + it('executes hooks without injection context', () => { + const messages: string[] = []; + const Store = signalStore( + withState({ name: 'NgRx Store' }), withHooks({ - onInit() { - inject(TOKEN); - messages.push('onInit'); + onInit(store) { + messages.push(`init ${store.name()}`); }, - onDestroy() { - // inject(TOKEN); - messages.push('onDestroy'); + onDestroy(store) { + messages.push(`destroy ${store.name()}`); }, }) ); const { destroy } = createLocalService(Store); - expect(messages).toEqual(['onInit']); + expect(messages).toEqual(['init NgRx Store']); destroy(); - expect(messages).toEqual(['onInit', 'onDestroy']); + expect(messages).toEqual(['init NgRx Store', 'destroy NgRx Store']); }); it('succeeds with onDestroy and providedIn: root', () => { diff --git a/modules/signals/src/with-hooks.ts b/modules/signals/src/with-hooks.ts index a927e0a2e7..df8c65d5dc 100644 --- a/modules/signals/src/with-hooks.ts +++ b/modules/signals/src/with-hooks.ts @@ -7,7 +7,7 @@ import { } from './signal-store-models'; import { Prettify } from './ts-helpers'; -type HooksFactory = ( +type HookFn = ( store: Prettify< SignalStoreSlices & Input['signals'] & @@ -16,11 +16,45 @@ type HooksFactory = ( > ) => void; +type HooksFactory = ( + store: Prettify< + SignalStoreSlices & + Input['signals'] & + Input['methods'] & + StateSignal> + > +) => { + onInit?: () => void; + onDestroy?: () => void; +}; + export function withHooks(hooks: { - onInit?: HooksFactory; - onDestroy?: HooksFactory; -}): SignalStoreFeature { + onInit?: HookFn; + onDestroy?: HookFn; +}): SignalStoreFeature; +export function withHooks( + hooks: HooksFactory +): SignalStoreFeature; + +export function withHooks( + hooksOrFactory: + | { + onInit?: HookFn; + onDestroy?: HookFn; + } + | HooksFactory +): SignalStoreFeature { return (store) => { + const storeProps = { + [STATE_SIGNAL]: store[STATE_SIGNAL], + ...store.slices, + ...store.signals, + ...store.methods, + }; + const hooks = + typeof hooksOrFactory === 'function' + ? hooksOrFactory(storeProps) + : hooksOrFactory; const createHook = (name: keyof typeof hooks) => { const hook = hooks[name]; const currentHook = store.hooks[name]; @@ -31,12 +65,7 @@ export function withHooks(hooks: { currentHook(); } - hook({ - [STATE_SIGNAL]: store[STATE_SIGNAL], - ...store.slices, - ...store.signals, - ...store.methods, - }); + hook(storeProps); } : currentHook; };