Signal Store

Preview — 3 of 10 questions

What is the NgRx Signal Store and how does it differ from the global store?

javascript
import { signalStore, withState, withComputed, withMethods, withHooks } from '@ngrx/signals';
import { withEntities, setAll, removeEntity, updateEntity } from '@ngrx/signals/entities';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { tapResponse } from '@ngrx/operators';

export const ProductStore = signalStore(
  { providedIn: 'root' },  // or scoped in component providers

  // State slice
  withState({
    loading: false,
    error: null as string | null,
    filter: 'all' as 'all' | 'active' | 'inactive',
  }),

  // Entity state (normalized collection)
  withEntities<Product>(),

  // Computed signals (derived state)
  withComputed(({ entities, filter }) => ({
    filteredProducts: computed(() => {
      const all = entities();
      return filter() === 'all' ? all : all.filter(p => p.status === filter());
    }),
    activeCount: computed(() => entities().filter(p => p.active).length),
  })),

  // Methods (updaters + effects)
  withMethods((store, productService = inject(ProductService)) => ({
    // Synchronous updater
    setFilter(filter: 'all' | 'active' | 'inactive') {
      patchState(store, { filter });
    },

    // Async effect using rxMethod
    loadProducts: rxMethod<void>(
      pipe(
        tap(() => patchState(store, { loading: true })),
        exhaustMap(() =>
          productService.getAll().pipe(
            tapResponse({
              next: products => patchState(store, setAll(products), { loading: false }),
              error: (err: Error) => patchState(store, { error: err.message, loading: false }),
            })
          )
        ),
      )
    ),
  })),

  // Lifecycle hooks
  withHooks({
    onInit(store) { store.loadProducts(); },
    onDestroy(store) { console.log('Store destroyed'); },
  }),
);

// Component usage:
@Component({ standalone: true })
export class ProductListComponent {
  store = inject(ProductStore);

  // All state is signals — no async pipe needed:
  // store.filteredProducts() — computed signal
  // store.loading()         — state signal
  // store.entities()        — entity signal
}
AIt is a wrapper around the global store that exposes state as signals
BA replacement for @ngrx/effects using signals instead of Observables
CA standalone, signal-first state management solution without actions/reducers boilerplate, using signalStore() factory with feature composition
DA global store that automatically converts all state values to signals

How do you create a reusable custom feature for NgRx Signal Store?

javascript
import { signalStoreFeature, withState, withMethods } from '@ngrx/signals';
import { patchState } from '@ngrx/signals';

// Reusable loading feature
interface LoadingState {
  loading: boolean;
  error: string | null;
}

export function withRequestStatus() {
  return signalStoreFeature(
    withState<LoadingState>({ loading: false, error: null }),
    withComputed(({ loading, error }) => ({
      isPending: loading,
      hasError: computed(() => error() !== null),
    })),
    withMethods(store => ({
      setLoading(): void {
        patchState(store, { loading: true, error: null });
      },
      setSuccess(): void {
        patchState(store, { loading: false, error: null });
      },
      setError(error: string): void {
        patchState(store, { loading: false, error });
      },
    })),
  );
}

// Reusable pagination feature
export function withPagination(initialPageSize = 20) {
  return signalStoreFeature(
    withState({ page: 1, pageSize: initialPageSize }),
    withMethods(store => ({
      nextPage(): void { patchState(store, { page: store.page() + 1 }); },
      prevPage(): void { patchState(store, { page: Math.max(1, store.page() - 1) }); },
      setPageSize(pageSize: number): void { patchState(store, { pageSize, page: 1 }); },
    })),
  );
}

// Using composed features:
export const ArticleStore = signalStore(
  withEntities<Article>(),
  withRequestStatus(),      // adds loading/error state and methods
  withPagination(10),       // adds pagination state and methods
  withMethods((store, articleService = inject(ArticleService)) => ({
    loadArticles: rxMethod<void>(pipe(
      tap(() => store.setLoading()),
      switchMap(() => articleService.getPage(store.page(), store.pageSize()).pipe(
        tapResponse({
          next: result => { patchState(store, setAll(result.articles)); store.setSuccess(); },
          error: (e: Error) => store.setError(e.message),
        }),
      )),
    )),
  })),
);
AWrite a function using signalStoreFeature() that returns withState, withComputed, and withMethods compositions
BCreate a class that extends SignalStoreFeature<T>
CExport a @NgModule with store features configured
DCustom features are not supported — compose with existing with* functions only

How would you implement a meta-reducer that sends analytics events for specific actions?

javascript
import { MetaReducer, ActionReducer, INIT } from '@ngrx/store';
import { AnalyticsService } from './analytics.service';

// Factory function — allows service injection
export function analyticsMetaReducerFactory(
  analyticsService: AnalyticsService
): MetaReducer<AppState> {
  return (reducer: ActionReducer<AppState>): ActionReducer<AppState> => {
    return (state, action) => {
      // Track specific actions:
      const trackedActions: string[] = [
        '[Cart] Item Added',
        '[Checkout] Purchase Completed',
        '[Auth] User Logged In',
      ];

      if (trackedActions.includes(action.type)) {
        // Synchronous analytics (e.g., Google Tag Manager dataLayer push)
        analyticsService.track(action.type, {
          timestamp: Date.now(),
          ...('payload' in action ? action : {}),
        });
      }

      return reducer(state, action);
    };
  };
}

// Registration:
export function provideAnalyticsMetaReducer(): EnvironmentProviders {
  return makeEnvironmentProviders([
    {
      provide: META_REDUCERS,
      useFactory: analyticsMetaReducerFactory,
      deps: [AnalyticsService],
      multi: true,
    },
  ]);
}

// In app.config.ts:
provideStore(reducers),
provideAnalyticsMetaReducer(),
AUse NgRx Effects with ofType() for all analytics actions
BWrap the reducer with a meta-reducer that intercepts specific actions and sends analytics synchronously before passing to the next reducer
CUse @ngrx/data to automatically track CRUD operations
DMeta-reducers cannot access services — use Effects for analytics

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.