Observable is pushed-based architecture. NgRx has two observable-based implementation, Store and ComponentStore. Store is more flexible and scalable, but you need deal with multiple objects such actions and reducers, selectors. ComponentStore is also powerful but much simpler to use. It involves less concepts and requires less coding.

All the source code of the post can be found here. The live demo is here

State in ComponentStore

Let’s implement the counter example using ComponentStore first.

export interface CmpStoreCounter {
  result: number;
}

@Injectable({
  providedIn: "root",
})
export class CmpStoreCounterService extends ComponentStore<CmpStoreCounter> {
  constructor() {
    super({ result: 0 });
  }

  result$ = this.select((state) => state.result);

  add = this.updater((state) => ({ ...state, result: state.result + 1 }));
}

@Component({
  selector: "app-ngrx-cmp-store-counter",
  template: `
    {{ logId() }}
    <h1>component store counter: {{ id }}</h1>
    <h2>Result: {{ result$ | async }}</h2>
    <button (click)="add()">Add</button>
  `,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CmpStoreCounterComponent extends Debug {
  constructor(private service: CmpStoreCounterService) {
    super();
  }

  result$ = this.service.result$;

  add() {
    this.service.add();
  }
}

To implement a ComponentStore, we only need to use updater to update the state, and the ComponentStore give your free observables for each member of state in store. Consuming ComponentStore in UI is very easy, as it has everything that UI needs, the update method and the state observable. The programming modal is very similar to the service implemented with BehaviorSubject. You can provide a component store as root service like the example here, or you can provide it as component level service.

State in Store

Now let’s implement counter example using NgRx store.

Let’s ignore how we implement the state first, but focus on how to consume the state in component.

@Component({
  selector: "app-store-counter",
  template: `
    {{ logId() }}
    <h1>Store counter: {{ id }}</h1>
    <h2>Result: {{ result$ | async }}</h2>
    <button (click)="add()">Add</button>
  `,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StoreCounterComponent extends Debug {
  constructor(private store: Store) {
    super();
  }

  result$ = this.store.select(selectCounterResult);

  add() {
    this.store.dispatch(actionAdd({ value: 1 }));
  }
}

Unlike ComponentStore, where the UI only need to deal with one ComponentStore object, here the UI need to deal with 3 kinds of objects.

  1. store
  2. selector
  3. action

Steps to consume store

  1. Create observable with selectors (read state)
  2. Dispatch actions (update state or trigger side effect).

To get the state in observable, you call store.select(selector). To update the state, you call store.dispatch(action(delta)). You need to remember what selector to use and what action to dispatch, which is not as easy as component store.

The following is how I implement actions, reducers, and selectors.

//1. define actions
export const actionAdd = createAction(
  "[counter] - add",
  props<{ value: number }>()
);

//2. define reducer (how to update the store)
export interface CounterStoreRoot {
  result: number;
}

export const counterReducer = createReducer(
  { result: 0 } as CounterStoreRoot,
  on(actionAdd, (state, action) => {
    return {
      result: action.value + state.result,
    };
  })
);

const counterStorePath = "counter";

//3. define selectors (exposing state as observable)
export const selectCounterStore =
  createFeatureSelector<CounterStoreRoot>(counterStorePath);

export const selectCounterResult = createSelector(
  selectCounterStore,
  (state) => state.result
);

//4. register reducer and effect
export const counterStoreModules = [
  StoreModule.forFeature(counterStorePath, calculationterReducer),
];

@NgModule({
  imports: [
    counterStoreModules,
    StoreModule.forRoot({}, {}),
    EffectsModule.forRoot([]),
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

The code is a bit messy, but all the reducer, selector, selectors are all very simple to implement

Steps to implement stores

  1. Define actions
  2. Define reducers / effects
  3. Define selectors
  4. Register reducers / effects.

Actions are free. They do not depend on reducers or anything. Selector is used to extract a state from store. Reducer is used to update a slice of the store. Both selectors and reducers are bound to a physical slice of the store. Reducers need to be registered to store to create state, and there are two way to do that.

Register reducer to root.

We need to add an entry in root object like below, everytime you register a new reducer.

@NgModule({
  imports: [
    StoreModule.forRoot({
      'counter': counterReducer,
      'path2', reducer2,
       //...
    }, {}),
    EffectsModule.forRoot([]),
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Register reducer as feature module.

This method you don’t need to modify the forRoot method, you just need to add a new feature module. And it is cleaner.

export const counterStoreModules = [
  StoreModule.forFeature(counterStorePath, calculationReducer),
];

@NgModule({
  imports: [
    counterStoreModules,
    StoreModule.forRoot({}, {}),
    EffectsModule.forRoot([]),
  ],
  bootstrap: [AppComponent],
})

Why separate action from reducer in Store.

In the Store case, there is the action(event), the reducer(handler). To update store, you dispatch an action, then you respond to the action with handler. The indirection seems to be very low efficiency. What benefit is that?

When the event and the event handler are separated, we can have many flexibilites. For example, we can have multiple handlers subscribe to a single event, and we can have one handler handle multiple events. Its many to many mapping.

But in the ComponentStore case, one event (the method name) is bound to one handler (updater). It is one to one mapping.

Think Reactively.

To take advantage of this separation, We need to change our programming mindset. We developer like to control and command. Using this mindset will not be fully take advantage of this flexibility.

If you like to control, you tend to use action as command. You will think like, “Hey counter state, please execute the add command to update yourself”. You tend to name the action '[counter] add'. You event name follow the pattern [state name] do something. If you think like this, using ComponentStore maybe be a better for you, because each update method map to its command name.

If you think reactively, you don’t want to command people to do something. You act implicitly. If you use action as event, you tend to think, “Hi there in case you are interested, my add button is clicked”. You will name your action as '[Page] - add button clicked'. If you think like this, you event name follow the pattern "[event_source] what happened"

The following is the code of better counter component, which using reactive mindset.

export const addButtonClicked = createAction(
  "[better counter component] - add button clicked",
  props<{ value: number }>()
);

export const betterCounterReducer = createReducer(
  { result: 0 } as BetterCounterStoreRoot,
  on(addButtonClicked, (state, action) => {
    return {
      result: action.value + state.result,
    };
  })
);

export class BetterStoreCounterComponent extends Debug {
  constructor(private store: Store) {
    super();
  }

  result$ = this.store.select(selectBetterCounterResult);

  add() {
    this.store.dispatch(addButtonClicked({ value: 1 }));
  }
}

There is literally no technical difference in new code other than the event renaming. But when you read your source code, or you debug with redux developer tool, you can easily know where this action is triggered. In the following image, which event name is debug friendly?

Imgur