Skip to content

15 NgRx

15.1 Overview

NgRx是 Angular 应用中实现全局状态管理的 Redux 架构解决方案。

Screenshot 2025-02-06 at 10.45.09

  1. @ngrx/store:全局状态管理模块
  2. @ngrx/effects:处理副作用
  3. @ngrx/store-devtools: 浏览器调试工具,需要依赖 Redux Devtools Extension
  4. @ngrx/schematics: 命令行工具,快速生成 NgRx文件
  5. @ngrx/entity:提高开发者在 Reducer 中操作数据的效率
  6. @ngrx/router-store: 将路由状态同步到全局 Store

ngrx 是一个单独的库, 需要哪儿个模块就用下哪儿个模块

15.2 Basic

  1. 下载 NgRx npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/router-store @ngrx/store-devtools @ngrx/schematics

or

npm install @ngrx/store@14 @ngrx/effects@14 @ngrx/entity@14 @ngrx/router-store@14 @ngrx/store-devtools@14 @ngrx/schematics@14

Screenshot 2025-02-10 at 20.19.16

  1. 配置 NgRx CLI

ng config cli.defaultCollection @ngrx/schematics

 // angular.json
 "cli": {
 "defaultCollection": "@ngrx/schematics"
 }

Screenshot 2025-02-10 at 20.21.03

  1. 创建 Store ng g store State --root --module app.module.ts --statePath store --stateInterface AppState
  angular-study git:(main)  ng g store State --root --module app.module.ts --statePath store --stateInterface AppState
Error: Unknown arguments: statePath, stateInterface
  angular-study git:(main)  ng g store State --root --module app.module.ts
CREATE src/app/reducers/index.ts (360 bytes)
UPDATE src/app/app.module.ts (9215 bytes)
  angular-study git:(main)  

为什么是 reducers 而不是 store

因为:

  1. NgRx 14+ 版本默认采用 reducers/ 作为全局状态存储目录,与官方推荐的目录结构一致。
  2. reducers/index.ts 作为 全局状态管理入口,可以在其中整合多个 feature store。
  3. reducers 这个名称符合 Redux/Ngrx 的设计理念,它通常表示 根级别的 reducer 组合(root reducer),其中可以包含多个 feature store 的 reducer。

Screenshot 2025-02-10 at 20.49.31

  1. 创建 Action ng g action store/actions/counter --skipTests

    import { createAction, props } from '@ngrx/store';
    
    // export const loadCounters = createAction(
    //   '[Counter] Load Counters'
    // );
    
    export const increment = createAction("increment")
    export const decrement = createAction("decrement")
    

    Screenshot 2025-02-10 at 20.52.46

  2. 创建 Reducer

ng g reducer store/reducers/counter --reducers=. /index.ts

import { Action, createReducer, on } from '@ngrx/store';
import { decrement, increment } from '../actions/counter.actions';
import { state } from '@angular/animations';

// 状态名称
export const counterFeatureKey = 'counter';
// 状态类型接口
export interface State {
  count: number
}
// 初始状态
export const initialState: State = {
  count: 0
};
// 创建reducer函数
export const reducer = createReducer(
  initialState,
  on(increment, (state) => ({
    ...state,
    count: state.count + 1
  })),
  on(decrement, state => ({
    ...state,
    count: state.count - 1
  }))
);

Screenshot 2025-02-11 at 09.33.09

  1. 创建Selector

ng g selector store/selectors/counter

src/app/store/selectors/counter.selectors.ts:

import { createFeatureSelector, createSelector } from '@ngrx/store';
// createFeatureSeletor 获取最外层的状态
// createSeletor 获取里层的状态
import { AppState } from '..';
import { counterFeatureKey, State } from '../reducers/counter.reducer';

export const selectCounterState = createFeatureSelector<AppState, State>(counterFeatureKey);
export const selectCount = createSelector(
    selectCounterState,
    state => state.count
);

src/app/components/ngrx/ngrx-store/ngrx-store.component.ts:

import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from 'src/app/store';

import { decrement, increment } from 'src/app/store/actions/counter.actions';
import { selectCount } from 'src/app/store/selectors/counter.selectors';

@Component({
  selector: 'app-ngrx-store',
  templateUrl: './ngrx-store.component.html',
  styleUrls: ['./ngrx-store.component.css']
})
export class NgrxStoreComponent implements OnInit {
  // counter: Observable<{ count: number }>;
  count: Observable<number>;
  constructor(private store: Store<AppState>) {
    // this.store.pipe(select("counter")).subscribe(console.log);
    // this.counter = this.store.pipe(select("counter"));
    this.count = this.store.pipe(select(selectCount));
  }

  ngOnInit(): void {
  }

  increment() {
    this.store.dispatch(increment());
  }

  decrement() {
    this.store.dispatch(decrement());
  }


}

src/app/components/ngrx/ngrx-store/ngrx-store.component.html:

<p>ngrx-store works!</p>
<button (click)="increment()">+1</button>
<!-- <span>{{(counter | async)?.count}}</span> -->
<span>{{ count | async }}</span>
<button (click)="decrement()">-1</button>

Screenshot 2025-02-11 at 10.31.24

Screenshot 2025-02-11 at 10.31.31

  1. 组件类触发Action, 获取状态

src/app/components/ngrx/ngrx-store/ngrx-store.component.html:

<p>ngrx-store works!</p>
<button (click)="increment()">+1</button>
<span>{{(counter | async)?.count}}</span>
<button (click)="decrement()">-1</button>

src/app/components/ngrx/ngrx-store/ngrx-store.component.ts:

import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { decrement, increment } from 'src/app/store/actions/counter.actions';

@Component({
  selector: 'app-ngrx-store',
  templateUrl: './ngrx-store.component.html',
  styleUrls: ['./ngrx-store.component.css']
})
export class NgrxStoreComponent implements OnInit {
  counter: Observable<{ count: number }>;
  constructor(private store: Store<AppState>) {
    // this.store.pipe(select("counter")).subscribe(console.log);
    this.counter = this.store.pipe(select("counter"));
  }

  ngOnInit(): void {
  }

  increment() {
    this.store.dispatch(increment());
  }

  decrement() {
    this.store.dispatch(decrement());
  }


}

Screenshot 2025-02-11 at 10.03.53

  1. 组件模版显示状态

src/app/components/ngrx/ngrx-store/ngrx-store.component.html:

<p>ngrx-store works!</p>
<button (click)="increment()">+1</button>
<span>{{(counter | async)?.count}}</span>
<button (click)="decrement()">-1</button>

为了方便区分

更改目录为store

src/app/store/index.ts:

import {
  ActionReducer,
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
import * as fromCounter from './reducers/counter.reducer';

// reducer中存储的状态类型接口
export interface AppState {

  [fromCounter.counterFeatureKey]: fromCounter.State;
}
// 状态名字和reducer的对应的关系
export const reducers: ActionReducerMap<AppState> = {

  [fromCounter.counterFeatureKey]: fromCounter.reducer,
};


export const metaReducers: MetaReducer<AppState>[] = !environment.production ? [] : [];

src/app/store/actions/counter.actions.ts:

import { createAction, props } from '@ngrx/store';

// export const loadCounters = createAction(
//   '[Counter] Load Counters'
// );

export const increment = createAction("increment")
export const decrement = createAction("decrement")

src/app/store/reducers/counter.reducer.ts:

import { Action, createReducer, on } from '@ngrx/store';
import { decrement, increment } from '../actions/counter.actions';
import { state } from '@angular/animations';

// 状态名称
export const counterFeatureKey = 'counter';
// 状态类型接口
export interface State {
  count: number
}
// 初始状态
export const initialState: State = {
  count: 0
};
// 创建reducer函数
export const reducer = createReducer(
  initialState,
  on(increment, (state) => ({
    ...state,
    count: state.count + 1
  })),
  on(decrement, state => ({
    ...state,
    count: state.count - 1
  }))
);

src/app/store/selectors/counter.selectors.ts:

import { createFeatureSelector, createSelector } from '@ngrx/store';
// createFeatureSeletor 获取最外层的状态
// createSeletor 获取里层的状态
import { AppState } from '..';
import { counterFeatureKey, State } from '../reducers/counter.reducer';

export const selectCounterState = createFeatureSelector<AppState, State>(counterFeatureKey);
export const selectCount = createSelector(
    selectCounterState,
    state => state.count
);

15.3 Action Payload

  1. 在组件中使用 dispatch 触发 Action 时传递参数,参数最终会被放置在 Action 对象中。
this. store.dispatch (increment({ count: 5 }))

src/app/components/ngrx/ngrx-action/ngrx-action.component.ts:

import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { decrement, increment } from 'src/app/store/actions/counter.actions';
import { selectCount } from 'src/app/store/selectors/counter.selectors';

@Component({
  selector: 'app-ngrx-action',
  templateUrl: './ngrx-action.component.html',
  styleUrls: ['./ngrx-action.component.css']
})
export class NgrxActionComponent implements OnInit {

  // counter: Observable<{ count: number }>;
  count: Observable<number>;
  constructor(private store: Store<AppState>) {
    this.count = this.store.pipe(select(selectCount));
  }

  ngOnInit(): void {
  }

  increment() {
    this.store.dispatch(increment({ count: 5 })); // 传递对象
  }

  decrement() {
    this.store.dispatch(decrement());
  }


}
  1. 在创建 Action Creator 函数时,获取参数并指定参数类型。
    import { createAction, props } from "@engry/store"
    export const increment = createAction("increment", props<{ count: number }>())
    
export declare function props<P extends object>(): Props<P>;

src/app/store/actions/counter.actions.ts:

import { createAction, props } from '@ngrx/store';

// export const loadCounters = createAction(
//   '[Counter] Load Counters'
// );

export const increment = createAction("increment", props<{ count: number }>());
export const decrement = createAction("decrement")
  1. 在Reducer 中通过 Action 对象获取参数。

src/app/store/reducers/counter.reducer.ts:

import { Action, createReducer, on } from '@ngrx/store';
import { decrement, increment } from '../actions/counter.actions';
import { state } from '@angular/animations';

// 状态名称
export const counterFeatureKey = 'counter';
// 状态类型接口
export interface State {
  count: number
}
// 初始状态
export const initialState: State = {
  count: 0
};
// 创建reducer函数
export const reducer = createReducer(
  initialState,
  on(increment, (state, action) => ({ // here
    ...state,
    // count: state.count + 1     
    count: state.count + action.count // here
  })),
  on(decrement, state => ({
    ...state,
    count: state.count - 1
  }))
);

Screenshot 2025-02-11 at 10.57.22

15.4 MetaReducer

metaReducer 是 Action-> Reducer 之间的钩子,允许开发者对 Action 进行预处理(在普通 Reducer 函数调用之前调用).

src/app/store/index.ts:

import {
  ActionReducer,
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
import * as fromCounter from './reducers/counter.reducer';

// reducer中存储的状态类型接口
export interface AppState {

  [fromCounter.counterFeatureKey]: fromCounter.State;
}
// 状态名字和reducer的对应的关系
export const reducers: ActionReducerMap<AppState> = {

  [fromCounter.counterFeatureKey]: fromCounter.reducer,
};

function logger(reducer: ActionReducer<AppState>): ActionReducer<AppState> {
  return function (state, action) {
    let result = reducer(state, action);
    console.log('lastest state ', result);
    console.log('last state', state);
    console.log('action', action);
    return reducer(state, action);
  };
}


export const metaReducers: MetaReducer<AppState>[] = !environment.production ? [logger] : [];

Screenshot 2025-02-11 at 11.06.43

15.5 Effect

需求: 在页面中新增一个按钮, 点击按钮后延迟一秒让数值增加.

  1. 在组件模板中新增一个用于异步数值增加的按钮,按钮被点击后执行 increment_async 方法

src/app/components/ngrx/ngrx-effect/ngrx-effect.component.html:

<p>ngrx-effect works!</p>
<button (click)="increment()">+1</button>
<button (click)="async_increment()">+1 after 2 seconds</button>
<!-- <span>{{(counter | async)?.count}}</span> -->
<span>{{ count | async }}</span>
<button (click)="decrement()">-1</button>
  1. 在组件类中新增 increment_async方法,并在方法中触发执行异步操作的 Action

src/app/components/ngrx/ngrx-effect/ngrx-effect.component.ts:

import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { async_increment, decrement, increment } from 'src/app/store/actions/counter.actions';
import { selectCount } from 'src/app/store/selectors/counter.selectors';

@Component({
  selector: 'app-ngrx-effect',
  templateUrl: './ngrx-effect.component.html',
  styleUrls: ['./ngrx-effect.component.css']
})
export class NgrxEffectComponent implements OnInit {

  // counter: Observable<{ count: number }>;
  count: Observable<number>;
  constructor(private store: Store<AppState>) {
    this.count = this.store.pipe(select(selectCount));
  }

  ngOnInit(): void {
  }

  increment() {
    this.store.dispatch(increment({ count: 5 }));
  }

  decrement() {
    this.store.dispatch(decrement());
  }

  async_increment() {
    this.store.dispatch(async_increment());
  }
}
  1. 在 Action 文件中新增执行异步操作的 Action src/app/store/actions/counter.actions.ts:
import { createAction, props } from '@ngrx/store';

// export const loadCounters = createAction(
//   '[Counter] Load Counters'
// );

export const increment = createAction("increment", props<{ count: number }>());
export const decrement = createAction("decrement")
export const async_increment = createAction("async_increment")
  1. 创建Effect,接收 Action 并执行副作用,继续触发 Action ng g effect store/effects/counter --root --module app.module.ts Effect 功能由 @ngrx/effects 模块提供,所以在根模块中需要导入相关的模块依赖

src/app/store/effects/counter.effects.ts:

import { Injectable } from '@angular/core';
import { Actions, createEffect } from '@ngrx/effects';

@Injectable()
export class CounterEffects {
  constructor(private actions$: Actions) { }
}

src/app/store/effects/counter.effects.ts:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map, merge, mergeMap, timer } from 'rxjs';
import { async_increment, increment } from '../actions/counter.actions';

// createEffect
// 用于创建 Effect,Effect 用于执行副作用•
// 调用方法时传递回调函数,回调函教中返回 Observable 对象,对象中要发出副作用执行完成后要触发的Action 对象
// 回调函数的返回值在 createEffect 方法内部被继续返回,最终返回值被存储在了 Effect 类的属性中
// NgRx 在实例化 Effect 类后,会订阅 Effect 类属性,当副作用执行完成后它会获取到要触发的 Action 对象并触发这个 Action

// Actions
// 当组件触发 Action 时,Bffect 需要通过 Actions 服务接收 Action,所以在 Effect 类中通过constructor 构造函数参数的方式将 Actions 服务类的实例对象注入到 Effect 类中
// Actions 服务类的实例对象为 observable 对象,当有 Action 被触发时,Action 对象本身会作为数据流被发出
// ofType
// 对目标 Action 对象进行过滤.
// 参数为目标 Action 的 Action creator 函数
// 如果未过滤出目标 Action 对象,本次不会继续发送数据流
// 如果过滤出目标 Action 对象,会将 Action 对象作为数据流继续发出

@Injectable()
export class CounterEffects {


  constructor(private actions$: Actions) {
    this.async_increment_effect.subscribe(console.log);
  }

  async_increment_effect = createEffect(() => {
    return this.actions$.pipe(
      ofType(async_increment),
      mergeMap(() => timer(2000).pipe(
        map(() => increment({ count: 10 }))
      ))
    )
  })
}

Screenshot 2025-02-11 at 12.16.38

15.6 Entity

15.6.1 Overview

Entity 译为实体,实体就是集合中的一条数据。

NgRx 中提供了实体适配器对象,在实体适配器对象下面提供了各种操作集合中实体的方法,目的就是提高开发者操作实体的效率。

ng g action store/actions/todo

import { createAction, props } from '@ngrx/store';

// 添加任务
export const addTodo = createAction("addTodo", props<{ title: string }>());

// 删除任务
export const deleteTodo = createAction("deleteTodo", props<{ id: string }>());

// export const loadTodos = createAction(
//   '[Todo] Load Todos'
// );

ng g reducer store/reducers/todo --reducers ../index.ts

npm install uuid@8.3.2

src/app/store/reducers/todo.reducer.ts:

import { Action, createReducer, on } from '@ngrx/store';
import { addTodo, deleteTodo } from '../actions/todo.actions';
import { v4 as uuidv4 } from 'uuid';


export const todoFeatureKey = 'todo';
export interface Todo {
  id: string
  title: string
}
export interface State {
  todos: Todo[]
}

export const initialState: State = {
  todos: []
};

export const reducer = createReducer(
  initialState,
  on(addTodo, (state, action) => ({
    ...state,
    todos: [...state.todos, { id: uuidv4(), title: action.title }]
  })),
  on(deleteTodo, (state, action) => {
    const newState: State = JSON.parse(JSON.stringify(state));
    const index = newState.todos.findIndex(todo => todo.id === action.id);
    newState.todos.splice(index, 1);
    return newState;
  })
);

src/app/store/actions/todo.actions.ts:

import { createAction, props } from '@ngrx/store';

// 添加任务
export const addTodo = createAction("addTodo", props<{ title: string }>());

// 删除任务
export const deleteTodo = createAction("deleteTodo", props<{ id: string }>());

// export const loadTodos = createAction(
//   '[Todo] Load Todos'
// );

src/app/components/ngrx/todo/todo.component.ts:

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { filter, fromEvent, map } from 'rxjs';
import { AppState } from 'src/app/store';
import { addTodo } from 'src/app/store/actions/todo.actions';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements AfterViewInit {
  @ViewChild("AddToDoInput") AddToDoInput!: ElementRef;

  constructor(private store: Store<AppState>) { }

  ngAfterViewInit() {
    fromEvent<KeyboardEvent>(this.AddToDoInput?.nativeElement, "keyup").pipe(
      filter(event => event.key === "Enter"),
      map(event => (<HTMLInputElement>event.target).value),
      map(title => title.trim()),
      filter(title => title !== "")
    ).subscribe(title => {
      this.store.dispatch(addTodo({ title }));
      this.AddToDoInput.nativeElement.value = "";
    });
  }

}

src/app/components/ngrx/todo/todo.component.html:

<p>todo works!</p>
<div class="container mt-4">
    <input #AddToDoInput type="text" class="form-control" placeholder="add todo">
    <ul class="list-group">
        <li class="list-group-item d-flex justify-content-between align-items-center">
            Task Name
            <button type="button" class="btn btn-danger btn-sm">
                Delete
            </button>
        </li>
    </ul>
</div>

ng g selector store/selectors/todo

src/app/store/selectors/todo.selectors.ts:

import { createFeature, createFeatureSelector, createSelector } from '@ngrx/store';
import { State, todoFeatureKey } from '../reducers/todo.reducer';
import { AppState } from '..';


export const selectTodo = createFeatureSelector<AppState, State>(todoFeatureKey);

export const selectTodos = createSelector(selectTodo, state => state.todos);

src/app/components/ngrx/todo/todo.component.ts:

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { filter, fromEvent, map, Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { addTodo } from 'src/app/store/actions/todo.actions';
import { Todo } from 'src/app/store/reducers/todo.reducer';
import { selectTodos } from 'src/app/store/selectors/todo.selectors';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements AfterViewInit {
  @ViewChild("AddToDoInput") AddToDoInput!: ElementRef;

  todos: Observable<Todo[]>


  constructor(private store: Store<AppState>) {
    this.todos = this.store.pipe(select(selectTodos));
  }

  ngAfterViewInit() {
    fromEvent<KeyboardEvent>(this.AddToDoInput?.nativeElement, "keyup").pipe(
      filter(event => event.key === "Enter"),
      map(event => (<HTMLInputElement>event.target).value),
      map(title => title.trim()),
      filter(title => title !== "")
    ).subscribe(title => {
      this.store.dispatch(addTodo({ title }));
      this.AddToDoInput.nativeElement.value = "";
    });
  }

}

src/app/components/ngrx/todo/todo.component.html:

<p>todo works!</p>
<div>
    <pre>
        {{ todos | async | json }}
    </pre>
</div>
<div class="container mt-4">
    <input #AddToDoInput type="text" class="form-control" placeholder="add todo">
    <ul class="list-group">
        <li *ngFor="let todo of todos | async"
            class="list-group-item d-flex justify-content-between align-items-center">
            {{todo.title}}
            <button type="button" class="btn btn-danger btn-sm">
                Delete
            </button>
        </li>
    </ul>
</div>

Screenshot 2025-02-11 at 13.46.31

delete:

src/app/components/ngrx/todo/todo.component.html:

<p>todo works!</p>
<div>
    <pre>
        {{ todos | async | json }}
    </pre>
</div>
<div class="container mt-4">
    <input #AddToDoInput type="text" class="form-control" placeholder="add todo">
    <ul class="list-group">
        <li *ngFor="let todo of todos | async"
            class="list-group-item d-flex justify-content-between align-items-center">
            {{todo.title}}
            <button (click)="deleteTodo(todo.id)" type="button" class="btn btn-danger btn-sm">
                Delete
            </button>
        </li>
    </ul>
</div>

src/app/components/ngrx/todo/todo.component.ts:

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { filter, fromEvent, map, Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { addTodo, deleteTodo } from 'src/app/store/actions/todo.actions';
import { Todo } from 'src/app/store/reducers/todo.reducer';
import { selectTodos } from 'src/app/store/selectors/todo.selectors';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements AfterViewInit {
  @ViewChild("AddToDoInput") AddToDoInput!: ElementRef;

  todos: Observable<Todo[]>


  constructor(private store: Store<AppState>) {
    this.todos = this.store.pipe(select(selectTodos));
  }

  ngAfterViewInit() {
    fromEvent<KeyboardEvent>(this.AddToDoInput?.nativeElement, "keyup").pipe(
      filter(event => event.key === "Enter"),
      map(event => (<HTMLInputElement>event.target).value),
      map(title => title.trim()),
      filter(title => title !== "")
    ).subscribe(title => {
      this.store.dispatch(addTodo({ title }));
      this.AddToDoInput.nativeElement.value = "";
    });
  }

  deleteTodo(id: string) {
    this.store.dispatch(deleteTodo({ id }));
  }

}

Screenshot 2025-02-11 at 13.48.33

15.6.2 Core

  1. EntityState:实体类型接口
/*
{
 ids: [1, 2],
 entities: {
     1: { id: 1, title: "Hello Angular"},
     2: { id: 2, title: "Hello NgRx"}
 }
}
*/

export interface State extends EntityState<Todo>{}
  1. createEntityAdapter: 创建实体适配器对象

  2. EntityAdapter:实体适配器对象类型接口

export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo> ()

// 获取初始状态 可以传递对象参数 也可以不传

// {ids: [1, entities: {}}
export const initialState: State = adapter getInitialState()

15.6.3 Adapter-collection-methods

https://ngrx.io/guide/entity/adapter#adapter-collection-methods

15.6.4 Entity Selector

The getSelectors method returned by the created entity adapter provides functions for selecting information from the entity.

The getSelectors method takes a selector function as its only argument to select the piece of state for a defined entity.

// selectTotal 获取数据条数
// selectA11 获取所有数据 以数组形式呈现
// selectEntities 获取实体集合 以字典形式呈现
// selectIds 获取ia集合以数组形式呈现
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();

src/app/store/selectors/todo.selectors.ts:

import { createFeature, createFeatureSelector, createSelector } from '@ngrx/store';
import { adapter, State, todoFeatureKey } from '../reducers/todo.reducer';
import { AppState } from '..';


export const selectTodo = createFeatureSelector<AppState, State>(todoFeatureKey);

// export const selectTodos = createSelector(selectTodo, state => state.todos);
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
export const selectTodos = createSelector(selectTodo, selectAll);

Screenshot 2025-02-11 at 14.31.27

src/app/store/selectors/todo.selectors.ts:

import { createFeature, createFeatureSelector, createSelector } from '@ngrx/store';
import { adapter, State, todoFeatureKey } from '../reducers/todo.reducer';
import { AppState } from '..';


export const selectTodo = createFeatureSelector<AppState, State>(todoFeatureKey);

// export const selectTodos = createSelector(selectTodo, state => state.todos);
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
export const selectTodos = createSelector(selectTodo, selectAll);
// export const selectTodos = createSelector(selectTodo, selectIds);

src/app/store/reducers/todo.reducer.ts:

import { Action, createReducer, on } from '@ngrx/store';
import { addTodo, deleteTodo } from '../actions/todo.actions';
import { v4 as uuidv4 } from 'uuid';
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';


export const todoFeatureKey = 'todo';
export interface Todo {
  id: string
  title: string
}
// export interface State {
//   todos: Todo[]
// }

export interface State extends EntityState<Todo> {
}

export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();



// export const initialState: State = {
//   todos: []
// };

export const initialState: State = adapter.getInitialState({});
// console.log(initialState);

// export const reducer = createReducer(
//   initialState,
//   on(addTodo, (state, action) => ({
//     ...state,
//     todos: [...state.todos, { id: uuidv4(), title: action.title }]
//   })),
//   on(deleteTodo, (state, action) => {
//     const newState: State = JSON.parse(JSON.stringify(state));
//     const index = newState.todos.findIndex(todo => todo.id === action.id);
//     newState.todos.splice(index, 1);
//     return newState;
//   })
// );
export const reducer = createReducer(
  initialState,
  on(addTodo, (state, action) => adapter.addOne({ id: uuidv4(), title: action.title }, state)),
  on(deleteTodo, (state, action) => adapter.removeOne(action.id, state))
);

src/app/components/ngrx/todo/todo.component.ts:

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { filter, fromEvent, map, Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { addTodo, deleteTodo } from 'src/app/store/actions/todo.actions';
import { Todo } from 'src/app/store/reducers/todo.reducer';
import { selectTodos } from 'src/app/store/selectors/todo.selectors';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements AfterViewInit {
  @ViewChild("AddToDoInput") AddToDoInput!: ElementRef;

  todos: Observable<Todo[]>


  constructor(private store: Store<AppState>) {
    this.todos = this.store.pipe(select(selectTodos));
  }

  ngAfterViewInit() {
    fromEvent<KeyboardEvent>(this.AddToDoInput?.nativeElement, "keyup").pipe(
      filter(event => event.key === "Enter"),
      map(event => (<HTMLInputElement>event.target).value),
      map(title => title.trim()),
      filter(title => title !== "")
    ).subscribe(title => {
      this.store.dispatch(addTodo({ title }));
      this.AddToDoInput.nativeElement.value = "";
    });
  }

  deleteTodo(id: string) {
    this.store.dispatch(deleteTodo({ id }));
  }

}

src/app/components/ngrx/todo/todo.component.html:

<p>todo works!</p>
<div>
    <pre>
        {{ todos | async | json }}
    </pre>
</div>
<div class="container mt-4">
    <input #AddToDoInput type="text" class="form-control" placeholder="add todo">
    <ul class="list-group">
        <li *ngFor="let todo of todos | async"
            class="list-group-item d-flex justify-content-between align-items-center">
            {{todo.title}}
            <button (click)="deleteTodo(todo.id)" type="button" class="btn btn-danger btn-sm">
                Delete
            </button>
        </li>
    </ul>
</div>

Screenshot 2025-02-11 at 14.45.13

15.7 Router Store

15.7.1 Synchronizing Router State with Store

  1. 引入模块

src/app/app.module.ts:

import { StoreRouterConnectingModule } from '@ngrx/router-store';

  // 声明当前模块依赖了哪些其他模块
  imports: [
    StoreRouterConnectingModule.forRoot(),
  ],
  1. 将路由状态集成到Store
import * as fromRouter from '@ngrx/router-store';
// reducer中存储的状态类型接口
export interface AppState {
  [fromCounter.counterFeatureKey]: fromCounter.State;
  [fromTodo.todoFeatureKey]: fromTodo.State;
  router: fromRouter.RouterReducerState // here
}
// 状态名字和reducer的对应的关系
export const reducers: ActionReducerMap<AppState> = {

  [fromCounter.counterFeatureKey]: fromCounter.reducer,
  [fromTodo.todoFeatureKey]: fromTodo.reducer,
  router: fromRouter.routerReducer      // here
};

15.7.2 创建获取路由状态的Selector

src/app/store/selectors/router.selectors.ts:

import { createFeatureSelector, createSelector } from '@ngrx/store';
import { AppState } from ".."
import { RouterReducerState, getSelectors } from "@ngrx/router-store"
const selectRouter = createFeatureSelector<AppState, RouterReducerState>(
    "router"
)
export const {
    //获取和当前路由相关的信息(路由参数、路由配置等)
    selectCurrentRoute,
    // 获取地址栏中,# 号后面的内容
    selectFragment,
    // 获取路由查询参数
    selectQueryParams,
    // 获取具体的某一个查询参数 selectOueryParam('name')
    selectQueryParam,
    // 获取动态路由参数
    selectRouteParams,
    // 获取某一个具体的动态路由参数 selectRouteParam('name')
    selectRouteParam,
    // 获取路由自定义数据
    selectRouteData,
    // 获取路由的实际访问地址
    selectUrl
} = getSelectors(selectRouter)

src/app/components/ngrx/todo/todo.component.ts:

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { filter, fromEvent, map, Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { addTodo, deleteTodo } from 'src/app/store/actions/todo.actions';
import { Todo } from 'src/app/store/reducers/todo.reducer';
import { selectCurrentRoute, selectFragment, selectQueryParam, selectQueryParams, selectRouteData } from 'src/app/store/selectors/router.selectors';
import { selectTodos } from 'src/app/store/selectors/todo.selectors';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements AfterViewInit {
  @ViewChild("AddToDoInput") AddToDoInput!: ElementRef;

  todos: Observable<Todo[]>


  constructor(private store: Store<AppState>) {
    this.todos = this.store.pipe(select(selectTodos));
    // this.store.pipe(select(selectCurrentRoute)).subscribe(console.log);
    // this.store.pipe(select(selectFragment)).subscribe(console.log);
    // this.store.pipe(select(selectQueryParams)).subscribe(console.log);
    this.store.pipe(select(selectQueryParam("name"))).subscribe(console.log);
    this.store.pipe(select(selectRouteData)).subscribe(console.log);
  }

  ngAfterViewInit() {
    fromEvent<KeyboardEvent>(this.AddToDoInput?.nativeElement, "keyup").pipe(
      filter(event => event.key === "Enter"),
      map(event => (<HTMLInputElement>event.target).value),
      map(title => title.trim()),
      filter(title => title !== "")
    ).subscribe(title => {
      this.store.dispatch(addTodo({ title }));
      this.AddToDoInput.nativeElement.value = "";
    });
  }

  deleteTodo(id: string) {
    this.store.dispatch(deleteTodo({ id }));
  }

}

Screenshot 2025-02-11 at 15.23.10