Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions modules/signals/events/spec/with-effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,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<string, unknown>[] = [];

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('dispatches an event with provided scope via toScope', () => {
const Store = signalStore(
{ providedIn: 'root' },
Expand Down
2 changes: 1 addition & 1 deletion modules/signals/events/src/with-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function withEffects<Input extends SignalStoreFeatureResult>(
Input['methods'] &
WritableStateSource<Prettify<Input['state']>>
>
) => Record<string, Observable<unknown>>
) => Record<string, Observable<unknown>> | Observable<unknown>[]
): SignalStoreFeature<Input, EmptyFeatureResult> {
return signalStoreFeature(
type<Input>(),
Expand Down
37 changes: 36 additions & 1 deletion projects/www/src/app/pages/guide/signals/signal-store/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -260,6 +260,41 @@ export const BookSearchStore = signalStore(

</ngrx-code-example>

<ngrx-docs-alert type="help">

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))),
])
);
```

</ngrx-docs-alert>

## Reading State

The Events plugin doesn’t change how the state is exposed or consumed.
Expand Down