15 NgRx
15.1 Overview
NgRx是 Angular 应用中实现全局状态管理的 Redux 架构解决方案。
- @ngrx/store:全局状态管理模块
- @ngrx/effects:处理副作用
- @ngrx/store-devtools: 浏览器调试工具,需要依赖 Redux Devtools Extension
- @ngrx/schematics: 命令行工具,快速生成 NgRx文件
- @ngrx/entity:提高开发者在 Reducer 中操作数据的效率
- @ngrx/router-store: 将路由状态同步到全局 Store
ngrx 是一个单独的库, 需要哪儿个模块就用下哪儿个模块
15.2 Basic
- 下载 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
- 配置 NgRx CLI
ng config cli.defaultCollection @ngrx/schematics
- 创建 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
?因为:
- NgRx 14+ 版本默认采用
reducers/
作为全局状态存储目录,与官方推荐的目录结构一致。reducers/index.ts
作为 全局状态管理入口,可以在其中整合多个 feature store。reducers
这个名称符合 Redux/Ngrx 的设计理念,它通常表示 根级别的 reducer 组合(root reducer),其中可以包含多个 feature store 的 reducer。
-
创建 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")
-
创建 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
}))
);
- 创建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>
- 组件类触发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());
}
}
- 组件模版显示状态
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
- 在组件中使用 dispatch 触发 Action 时传递参数,参数最终会被放置在 Action 对象中。
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());
}
}
- 在创建 Action Creator 函数时,获取参数并指定参数类型。
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")
- 在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
}))
);
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] : [];
15.5 Effect
需求: 在页面中新增一个按钮, 点击按钮后延迟一秒让数值增加.
- 在组件模板中新增一个用于异步数值增加的按钮,按钮被点击后执行
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>
- 在组件类中新增
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());
}
}
- 在 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")
- 创建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 }))
))
)
})
}
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>
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 }));
}
}
15.6.2 Core
- EntityState:实体类型接口
/*
{
ids: [1, 2],
entities: {
1: { id: 1, title: "Hello Angular"},
2: { id: 2, title: "Hello NgRx"}
}
}
*/
export interface State extends EntityState<Todo>{}
-
createEntityAdapter: 创建实体适配器对象
-
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);
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>
15.7 Router Store
15.7.1 Synchronizing Router State with Store
- 引入模块
src/app/app.module.ts
:
import { StoreRouterConnectingModule } from '@ngrx/router-store';
// 声明当前模块依赖了哪些其他模块
imports: [
StoreRouterConnectingModule.forRoot(),
],
- 将路由状态集成到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 }));
}
}