Classic NgRx

The same transactions feature, modeled the classic way: explicit actions, a pure reducer, async effects, and memoized selectors. Where a SignalStore folds intent and state into method calls on one object, here every change is a serializable event flowing through a one-way pipeline. It's verbose, but traceable — you can replay each event in the Redux DevTools. All figures are synthetic and deterministic.

Transactions

Net total$0
Loading transactions
Dispatching Transactions / Load — the effect is resolving the data source.
View source — Transactions feature

One feature, three files. An action is dispatched, an effect catches it and loads the data, the reducer folds that result into state, and selectors read it back out — a one-way loop you can trace end to end.

ts
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import type { Transaction } from '@foo/util-data';

/*
 * Actions are the vocabulary of a classic-NgRx feature: every change the system can undergo,
 * declared up front as a plain, serializable event. This is the philosophical split from the
 * SignalStore in the sibling package. There you call a method and state changes. Here nothing
 * changes state directly — code *dispatches* a fact ("Load was requested," "Load succeeded"),
 * and a reducer somewhere else decides what that fact does to the state.
 *
 * That indirection costs you a file and a few keystrokes, and it buys three things: every
 * change is an object you can log, time-travel, and replay in the Redux DevTools; the thing
 * triggering a change is fully decoupled from the thing applying it; and the action list reads
 * as documentation of everything that can ever happen to this slice. On a small feature that
 * tax looks like ceremony — on a large one with many triggers feeding one reducer, the audit
 * trail is what keeps it debuggable.
 *
 * Note the async trio. The component only ever fires `load` — the bare intent. It does NOT
 * know about success or failure; the effect owns those. Splitting one user action into
 * request/success/failure is the pattern that lets the reducer model loading, data, and error
 * as three clean transitions instead of one tangled callback.
 *
 * The `source` prefixes every type ("[Transactions] Load"), so a glance at the DevTools log
 * tells you which feature an action came from.
 */
export const TransactionsActions = createActionGroup({
  source: 'Transactions',
  events: {
    /*
     * Title-case keys here become camelCase creators ("Load Success" -> `loadSuccess`).
     * `emptyProps()` is an action that carries no payload; `props<{...}>()` types the payload
     * an action must be dispatched with, so a typo'd or missing field fails at compile time.
     */
    Load: emptyProps(),
    'Load Success': props<{ transactions: Transaction[] }>(),
    'Load Failure': props<{ error: string }>(),
    'Set Category': props<{ category: string }>(),
  },
});