Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Signal Store Workshop

Avatar for Felix Wunderlich Felix Wunderlich
October 17, 2026
8

Signal Store Workshop

Avatar for Felix Wunderlich

Felix Wunderlich

October 17, 2026

Transcript

  1. Angular Days State Management with NgRx Signal Store Manage state

    in a reactive way Felix Wunderlich Consultant
  2. Angular Days @ngrx/signals: Reactive State Management in Angular Felix Wunderlich

    Hi, that’s me. Expert in Angular and Generative AI Thinktecture AG E-Mail [email protected] LinkedIn linkedin.com/in/felixwdl Profile thinktecture.com/thinktects/felix-schuetz 2
  3. Angular Days @ngrx/signals: Reactive State Management in Angular Agenda Part

    1 · Signal Store basics Why state management? NgRx and the Signal Store Defining and using a store Dependencies and lifecycle hooks Lab 1: Todo store (bonus lab: Watchlist store) Part 2 · Async, events, architecture Async state with rxMethod Entities and custom features Lab 2: Media store Signal Store Events Architecture and conclusion 3
  4. Angular Days @ngrx/signals: Reactive State Management in Angular Schedule 9:30

    – 13:00, 2 × 90 minutes 11:30 – 13:00 Part 1: Signal Store basics Part 2: Async, events, architecture 90 min · Lab 1 03:11 – 00:11 · kaerB 9:30 – 11:00 90 min · Lab 2 4
  5. Angular Days @ngrx/signals: Reactive State Management in Angular Signals Everywhere

    Angular 22 is signal-first, from inputs to forms Primitives , signal computed untracked , effect , linkedSignal , Signal-based component API , , input model output contentChild(ren) , viewChild(ren) , n = signal(0); double = computed(() => n() * 2); todo = input.required<Todo>(); done = output<string>(); Async Forms , httpResource , rxResource Interop: toSignal , toObservable resource movies = httpResource(url); Signal Forms: form over a signal with a schema, stable since 22 login = form(this.credentials, schema); 5
  6. Angular Days @ngrx/signals: Reactive State Management in Angular Why State

    Management with Stores? Four forces that push state out of the component Web apps have different kinds of state UI state, storage state, data state, URL state Multiple data sources Services, WebSockets, local storage, … Multiple developers Everyone has their own idea of where state lives Different solutions for the same problems Loading flags, error handling and caching reinvented per feature 7
  7. Angular Days @ngrx/signals: Reactive State Management in Angular State Without

    a Plan Every component, service and developer solves it differently 8
  8. Angular Days @ngrx/signals: Reactive State Management in Angular State Management

    Solutions In Angular you have four options No state management State lives in components and is passed around with inputs and outputs RxJS-based BehaviorSubjects in services, read-only observables for consumers Signal-based Signals in services, computed for derived state State management libraries NgRx (Store, Component Store, Signal Store), NGXS, … 9
  9. Angular Days @ngrx/signals: Reactive State Management in Angular RxJS-Based Store

    A service with a BehaviorSubject State is handled internally Accessible through a readonly observable Subscription with the async pipe Immutable changes @Injectable() export class TodoStore { private readonly todos = new BehaviorSubject<Todo[]>([]); readonly todos$ = this.todos.asObservable(); readonly completedTodos$ = this.todos$.pipe( map((todos) => todos.filter((todo) => todo.completed)), ); addTodo(title: string): void { const todo = { id: crypto.randomUUID(), title, completed: false }; this.todos.next([...this.todos.value, todo]); } } 10
  10. Angular Days @ngrx/signals: Reactive State Management in Angular Signal-Based Store

    The same service with signals State is handled internally No subscription handling needed No third-party library needed Derived state with computed @Injectable() export class TodoStore { private readonly _todos = signal<Todo[]>([]); readonly todos = this._todos.asReadonly(); readonly completedTodos = computed(() => this._todos().filter((todo) => todo.completed), ); addTodo(title: string): void { const todo = { id: crypto.randomUUID(), title, completed: false }; this._todos.update((todos) => [...todos, todo]); } } 11
  11. Angular Days @ngrx/signals: Reactive State Management in Angular NgRx The

    de-facto standard in Angular Redux pattern in Angular: @ngrx/store , actions, reducers, effects, selectors Reactive with observables or signals Component Store as a lightweight alternative for componentbased state Signal Store: the signal-first store since NgRx 17, stable since 18 Events plugin: event-driven flows for the Signal Store, stable since NgRx 21 13
  12. Angular Days @ngrx/signals: Reactive State Management in Angular The Redux

    Lifecycle Component › Action › Reducer › Store › Selector Selector reads Store Database new state Reducer renders Service reduces Component dispatches Action side effects Effects 14
  13. Angular Days @ngrx/signals: Reactive State Management in Angular Dispatch, Reduce,

    Render The same idea, drawn as a loop Event handler 2 · dispatch deposit(10) dispatch(deposit(10)) 1 · click UI Deposit $10 Withdraw $10 $10 Store Reducer Reducer Reducer deposit withdraw reset balance += 10 balance -= 10 balance = 0 3 · reduce balance: 10 balance: 0 4 · render State balance: 10 15
  14. Angular Days @ngrx/signals: Reactive State Management in Angular NgRx Store

    vs. Signal Store Two models, both maintained Scope State changes Side effects Reading state Code per feature Devtools Pick it when NgRx Store Global by design Actions and reducers Effects Selectors, selectSignal Actions, reducer, selectors, effects @ngrx/store-devtools : action log, time travel Large existing codebase, global action log required Signal Store Component, feature or global patchState and methods, reducers via the events plugin rxMethod , event handlers Signals and computed One store file from ngrx-toolkit, events via withDevtools withTrackedReducer New apps, scoped state, less ceremony 16
  15. Angular Days @ngrx/signals: Reactive State Management in Angular NgRx Signal

    Store Lightweight reactive state management Standalone library: @ngrx/signals Signal-based Functional and composable API Extensible with custom features RxJS compatible Helper functions: signalState , deepComputed , signalMethod , rxMethod 18
  16. Angular Days @ngrx/signals: Reactive State Management in Angular Installation One

    package, two optional companions # Signal Store npm install @ngrx/signals # Operators such as tapResponse and mapResponse npm install @ngrx/operators # ESLint rules for the Signal Store (optional) npm install --save-dev @ngrx/eslint-plugin 19
  17. Angular Days @ngrx/signals: Reactive State Management in Angular Signal Store

    Features A store is composed from features creates the store withState holds the state withComputed derives from it withMethods changes it withProps shares dependencies withHooks reacts to the lifecycle withEntities manages collections signalStoreFeature builds your own signalStore export const TodoStore = signalStore( { providedIn: 'root' }, withState({}), withComputed(() => ({})), withMethods(() => ({})), withProps(() => ({})), withHooks(() => ({})), ); 20
  18. Angular Days @ngrx/signals: Reactive State Management in Angular Define the

    Store State, computed values and methods export const TodoStore = signalStore( withState<TodoState>({ todos: [] }), withComputed(({ todos }) => ({ todosCount: computed(() => todos().length), completedTodos: computed(() => todos().filter((todo) => todo.completed)), })), withMethods((store) => ({ addTodo: (title: string) => { const todo = { id: crypto.randomUUID(), title, completed: false }; patchState(store, { todos: [...store.todos(), todo] }); }, toggleTodo: (id: string) => patchState(store, (state) => ({ todos: state.todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)), })), })), ); 21
  19. Angular Days @ngrx/signals: Reactive State Management in Angular Use the

    Store Inject it, read signals, call methods inject() the store like a service State and computed values are signals Methods are plain functions Provide it per component, or providedIn: 'root' for a singleton @Component({ selector: 'app-todos', providers: [TodoStore], template: ` <p>{{ store.todosCount() }} todos</p> @for (todo of store.todos(); track todo.id) { <app-todo [todo]="todo" (toggle)="store.toggleTodo(todo.id)" /> } `, }) export class Todos { protected readonly store = inject(TodoStore); } 22
  20. Angular Days @ngrx/signals: Reactive State Management in Angular Share Dependencies

    withProps holds what is not state Share injectables or other dependencies Available across the store without re-injecting Also for derived observables or static values Prefix with _ to keep them private export const TodoStore = signalStore( withState<TodoState>({ todos: [] }), withComputed(({ todos }) => ({ todosCount: computed(() => todos().length), })), withProps(({ todosCount }) => ({ todosCount$: toObservable(todosCount), _todoService: inject(TodoService), })), withMethods((store) => ({ loadTodos: () => store._todoService.loadTodos(), })), ); 23
  21. Angular Days @ngrx/signals: Reactive State Management in Angular Lifecycle Hooks

    and Watch State withHooks runs on init and destroy runs when the store is created onDestroy runs when its injector is destroyed watchState runs on every state change Typical uses: initial load, persistence, logging onInit export const TodoStore = signalStore( withState<TodoState>({ todos: [] }), withProps(() => ({ todoService: inject(TodoService) })), withHooks((store) => ({ onInit: () => { watchState(store, (state) => { console.log('[watchState] todo state', state); }); store.todoService.loadTodos(); }, onDestroy: () => store.todoService.clearTodos(), })), ); 24
  22. Lab Lab 1: Todo Store Refactor the Todo component to

    use a Signal Store github.com/thinktecture-labs/signal-store-workshop exercises/01-todo-store 25
  23. Lab Bonus Lab: Watchlist Store Convert the WatchlistService to a

    Signal Store github.com/thinktecture-labs/signal-store-workshop exercises/02-watchlist-store 26
  24. Angular Days @ngrx/signals: Reactive State Management in Angular rxMethod Reactive

    methods with RxJS inside the store Opt-in integration with RxJS Manages side effects such as HTTP calls Accepts a value, a signal or an observable as input No manual subscription required, cleaned up with the store Can be used standalone without the store export const TodoStore = signalStore( withState<TodoState>({ todos: [] }), withProps(() => ({ todoService: inject(TodoService) })), withMethods((store) => ({ loadTodos: rxMethod<void>( switchMap(() => store.todoService.getTodos().pipe( tapResponse({ next: (todos) => patchState(store, { todos }), error: (error) => console.error(error), }), ), ), ), })), ); 29
  25. Angular Days @ngrx/signals: Reactive State Management in Angular Resource Extensions

    Better resource values, experimental in NgRx 22 Angular resources reset value() while reloading and throw on error extendResource from @ngrx/signals/resource patches that behaviour Keep the previous value or return a fallback, while loading and on error Defaults per app, route or component with export const BookStore = signalStore( withState({ query: '' }), withProps(({ query }) => ({ _books: extendResource( httpResource<Book[]>(() => `/api/books?q=${query()}`), withPreviousValueOnLoading(), withValueOnError([]), ), })), withComputed(({ _books }) => ({ books: computed(() => _books.value() ?? []), isLoading: _books.isLoading, })), ); provideResourceExtensions 30
  26. Angular Days @ngrx/signals: Reactive State Management in Angular Entities Collections

    without the boilerplate A collection of entities with ids Updaters: addEntity , updateEntity , removeEntity , … Read entities() , entityMap() , ids() Normalised state, no nested arrays export const TodoStore = signalStore( withEntities<Todo>(), withMethods((store) => ({ addTodo: (title: string) => { const todo = { id: crypto.randomUUID(), title, completed: false }; patchState(store, addEntity(todo)); }, removeTodo: (id: string) => patchState(store, removeEntity(id)), })), ); Optional: a named collection with entityConfig const todoEntity = entityConfig({ entity: type<Todo>(), collection: 'todo' }); withEntities(todoEntity), // todoEntities(), todoEntityMap(), todoIds() patchState(store, addEntity(todo, todoEntity)); 31
  27. Angular Days @ngrx/signals: Reactive State Management in Angular Custom Signal

    Store Features Reusable, type-safe building blocks Reusable features with signalStoreFeature Type safe: declare what the feature requires Atomic updates with custom updater functions Ready-made features: ngrx-toolkit type SelectedEntityState = { selectedEntityId: EntityId | null }; export function withSelectedEntity<Entity>() { return signalStoreFeature( { state: type<EntityState<Entity>>() }, withState<SelectedEntityState>({ selectedEntityId: null }), withComputed(({ entityMap, selectedEntityId }) => ({ selectedEntity: computed(() => { const selectedId = selectedEntityId(); return selectedId ? entityMap()[selectedId] : null; }), })), ); } 32
  28. Angular Days @ngrx/signals: Reactive State Management in Angular Atomic Updates

    Combine updaters in one patchState call export function selectEntity( selectedEntityId: EntityId | null, ): SelectedEntityState { return { selectedEntityId }; } addTodo: (title: string) => { const todo = { id: '1', title, completed: false }; ❌ // Two updates, two change notifications patchState(store, addEntity(todo)); patchState(store, { selectedEntityId: '1' }); ✅ // One atomic update patchState( store, addEntity(todo), selectEntity('1'), ); }, 33
  29. Lab Lab 2: Media Store Refactor the media overview and

    search to use a Signal Store github.com/thinktecture-labs/signal-store-workshop exercises/03-media-store 34
  30. Angular Days @ngrx/signals: Reactive State Management in Angular State Over

    Time What the Redux DevTools record for one search 1 · Trending loaded { 2 · Query typed { query: '', movies: [/* 20 trending */], shows: [/* 20 trending */], moviesLoading: false, showsLoading: false, } 3 · Results arrived { query: 'Spider', movies: [/* 20 trending */], shows: [/* 20 trending */], moviesLoading: true, showsLoading: true, } query: 'Spider', movies: [/* 20 results */], shows: [/* 20 results */], moviesLoading: false, showsLoading: false, } 36
  31. Angular Days @ngrx/signals: Reactive State Management in Angular Signal Store

    Events Flux for the Signal Store Event: describes an occurrence within the system, dispatched to trigger state changes or side effects Dispatcher: an event bus that forwards events to their handlers in the stores Store: reducers and event handlers that manage state and side effects View: reflects state changes and dispatches new events Event Dispatcher Store reducer, event handlers dispatches events View 38
  32. Angular Days @ngrx/signals: Reactive State Management in Angular Events Define

    events, alone or as a group import { type } from '@ngrx/signals'; import { event, eventGroup } from '@ngrx/signals/events'; import { Book } from './book'; export const loadedSuccess = event('[Books API] Loaded Success', type<Book[]>()); export const loadedFailure = event('[Books API] Loaded Failure', type<string>()); Alternative: an event group export const booksApiEvents = eventGroup({ source: 'Books API', events: { loadedSuccess: type<Book[]>(), loadedFailure: type<string>(), }, }); 39
  33. Angular Days @ngrx/signals: Reactive State Management in Angular Reducer withReducer

    maps events to state changes export const BookSearchStore = signalStore( withState<State>({ query: '', books: [], isLoading: false }), withReducer( on(bookSearchEvents.opened, () => ({ isLoading: true })), on(bookSearchEvents.queryChanged, ({ payload: query }) => ({ query, isLoading: true })), on(booksApiEvents.loadedSuccess, ({ payload: books }) => ({ books, isLoading: false })), on(booksApiEvents.loadedFailure, () => ({ isLoading: false })), ), ); 40
  34. Angular Days @ngrx/signals: Reactive State Management in Angular Event Handlers

    withEventHandlers runs the side effects export const BookSearchStore = signalStore( // withState and withReducer as on the previous slide withEventHandlers((store, events = inject(Events), booksService = inject(BooksService)) => ({ loadBooksByQuery$: events.on(bookSearchEvents.opened, bookSearchEvents.queryChanged).pipe( switchMap(() => booksService.getByQuery(store.query()).pipe( mapResponse({ next: (books) => booksApiEvents.loadedSuccess(books), error: (error: { message: string }) => booksApiEvents.loadedFailure(error.message), }), ), ), ), })), ); 41
  35. Angular Days @ngrx/signals: Reactive State Management in Angular Dispatch Events

    The view talks to the dispatcher export class BookSearch { readonly dispatcher = inject(Dispatcher); readonly store = inject(BookSearchStore); constructor() { this.dispatcher.dispatch(bookSearchEvents.opened()); } changeQuery(query: string): void { this.dispatcher.dispatch(bookSearchEvents.queryChanged(query)); } } 42
  36. Angular Days @ngrx/signals: Reactive State Management in Angular Dispatch Events

    with injectDispatch Less boilerplate in the view takes a dictionary of event creators It returns an object with the same shape, every method creates and dispatches the event injectDispatch export const bookSearchEvents = eventGroup({ source: 'Book Search', events: { opened: type<void>(), queryChanged: type<string>(), }, }); export class BookSearch { readonly dispatch = injectDispatch(bookSearchEvents); readonly store = inject(BookSearchStore); constructor() { this.dispatch.opened(); } changeQuery(query: string): void { this.dispatch.queryChanged(query); } } 43
  37. Angular Days @ngrx/signals: Reactive State Management in Angular Signal Store

    Architecture Small stores, close to where the state is used Avoid global stores that share everything with everyone Split larger stores into smaller stores Separation of concerns: one store per feature or component Share functionality through features instead of store injections injects injects Global state injects Feature state A Feature state B Component state Component state Component state Component state injects 45
  38. Angular Days @ngrx/signals: Reactive State Management in Angular Conclusion What

    to take home The Signal Store provides a lightweight, reactive state management solution ✓ Built on top of the Angular Signals API ✓ Can be used on a component, feature or global level ✓ Does not replace Redux: for event-driven flows there is the events plugin ✓ Reusable behaviour through Signal Store Features ✓ 46