From a6dc25dd1d9b205290e61648f16179089cfd57df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Stanimirovi=C4=87?= Date: Mon, 24 Nov 2025 23:05:14 +0100 Subject: [PATCH] feat(signals): allow returning an array of observables from withEffects --- .../signals/events/spec/with-effects.spec.ts | 27 ++++++++++++++ modules/signals/events/src/with-effects.ts | 2 +- .../guide/signals/signal-store/events.md | 37 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/modules/signals/events/spec/with-effects.spec.ts b/modules/signals/events/spec/with-effects.spec.ts index aa7313d4cb..5c4d52cf16 100644 --- a/modules/signals/events/spec/with-effects.spec.ts +++ b/modules/signals/events/spec/with-effects.spec.ts @@ -127,6 +127,33 @@ describe('withEffects', () => { ]); }); + it('handles observables returned as an array', () => { + const Store = signalStore( + { providedIn: 'root' }, + withEffects((_, events = inject(Events)) => [ + events.on(event1, event2).pipe(map(({ type }) => event3(type))), + events.on(event3).pipe(tap(() => {})), + ]) + ); + + const events = TestBed.inject(Events); + const dispatcher = TestBed.inject(Dispatcher); + const emittedEvents: EventInstance[] = []; + + events.on().subscribe((event) => emittedEvents.push(event)); + TestBed.inject(Store); + + dispatcher.dispatch(event1()); + dispatcher.dispatch(event2()); + + expect(emittedEvents).toEqual([ + { type: 'event1' }, + { type: 'event3', payload: 'event1' }, + { type: 'event2' }, + { type: 'event3', payload: 'event2' }, + ]); + }); + it('unsubscribes from effects when the store is destroyed', () => { let executionCount = 0; diff --git a/modules/signals/events/src/with-effects.ts b/modules/signals/events/src/with-effects.ts index 3cfd978383..d44636c4d5 100644 --- a/modules/signals/events/src/with-effects.ts +++ b/modules/signals/events/src/with-effects.ts @@ -49,7 +49,7 @@ export function withEffects( Input['methods'] & WritableStateSource> > - ) => Record> + ) => Record> | Observable[] ): SignalStoreFeature { return signalStoreFeature( type(), diff --git a/projects/www/src/app/pages/guide/signals/signal-store/events.md b/projects/www/src/app/pages/guide/signals/signal-store/events.md index 1ddd839fc9..fbf100de8a 100644 --- a/projects/www/src/app/pages/guide/signals/signal-store/events.md +++ b/projects/www/src/app/pages/guide/signals/signal-store/events.md @@ -215,7 +215,7 @@ function incrementSecond(): PartialStateUpdater<{ count2: number }> { ## Performing Side Effects Side effects are handled using the `withEffects` feature. -This feature accepts a function that receives the store instance as an argument and returns a dictionary of effects. +This feature accepts a function that receives the store instance as an argument and returns either a dictionary of effects or an array of effects. Each effect is defined as an observable that reacts to specific events using the `Events` service. This service provides the `on` method that returns an observable of dispatched events filtered by the specified event types. If an effect returns a new event, that event is automatically dispatched. @@ -260,6 +260,41 @@ export const BookSearchStore = signalStore( + + +In addition to the `Events` service, effects can be defined by listening to any other observable source. +It's also possible to return an array of effects from the `withEffects` feature. + +```ts +// ... other imports +import { exhaustMap, tap, timer } from 'rxjs'; +import { withEffects } from '@ngrx/signals/events'; +import { mapResponse } from '@ngrx/operators'; +import { BooksService } from './books-service'; + +export const BookSearchStore = signalStore( + // ... other features + withEffects((store, booksService = inject(BooksService)) => [ + timer(0, 30_000).pipe( + exhaustMap(() => + booksService.getAll().pipe( + mapResponse({ + next: (books) => booksApiEvents.loadedSuccess(books), + error: (error: { message: string }) => + booksApiEvents.loadedFailure(error.message), + }) + ) + ) + ), + events + .on(booksApiEvents.loadedFailure) + .pipe(tap(({ payload }) => console.error(payload))), + ]) +); +``` + + + ## Reading State The Events plugin doesn’t change how the state is exposed or consumed.