Angular 状态管理
Angular 状态管理
现代 Angular 状态管理模式的全面指南,涵盖从基于 Signal 的局部状态到全局 Store 以及服务器状态同步。
何时使用此技能
- 在 Angular 中搭建全局状态管理
- 在 Signals、NgRx 或 Akita 之间做出选择
- 管理组件级 Store
- 实现乐观更新 (Optimistic Updates)
- 调试状态相关问题
- 从旧的状态模式进行迁移
何时不要使用此技能
- 任务与 Angular 状态管理无关
- 需要 React 状态管理 $\rightarrow$ 使用
react-state-management
---
核心概念
状态分类
| 类型 | 描述 | 解决方案 |
| ---------------- | ---------------------------- | --------------------- |
| 局部状态 (Local State) | 组件特定,UI 状态 | Signals, signal() |
| 共享状态 (Shared State) | 相关组件之间共享 | Signal services |
| 全局状态 (Global State) | 全应用范围,复杂状态 | NgRx, Akita, Elf |
| 服务器状态 (Server State) | 远程数据,缓存 | NgRx Query, RxAngular |
| URL 状态 (URL State) | 路由参数 | ActivatedRoute |
| 表单状态 (Form State) | 输入值,校验 | Reactive Forms |
选择标准
小型应用,简单状态 $\rightarrow$ Signal Services
中型应用,中等状态 $\rightarrow$ Component Stores
大型应用,复杂状态 $\rightarrow$ NgRx Store
频繁服务器交互 $\rightarrow$ NgRx Query + Signal Services
实时更新 $\rightarrow$ RxAngular + Signals---
快速上手:基于 Signal 的状态
模式 1:简单 Signal 服务
// services/counter.service.ts
import { Injectable, signal, computed } from "@angular/core";
@Injectable({ providedIn: "root" })
export class CounterService {
// 私有可写 signal
private _count = signal(0);
// 公开只读 signal
readonly count = this._count.asReadonly();
readonly doubled = computed(() => this._count() * 2);
readonly isPositive = computed(() => this._count() > 0);
increment() {
this._count.update((v) => v + 1);
}
decrement() {
this._count.update((v) => v - 1);
}
reset() {
this._count.set(0);
}
}
// 在组件中使用
@Component({
template:
<p>Count: {{ counter.count() }}</p>
<p>Doubled: {{ counter.doubled() }}</p>
<button (click)="counter.increment()">+</button>
,
})
export class CounterComponent {
counter = inject(CounterService);
}
模式 2:功能性 Signal Store
// stores/user.store.ts
import { Injectable, signal, computed, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";
interface User {
id: string;
name: string;
email: string;
}
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
@Injectable({ providedIn: "root" })
export class UserStore {
private http = inject(HttpClient);
// 状态 signals
private _user = signal<User | null>(null);
private _loading = signal(false);
private _error = signal<string | null>(null);
// 选择器 (只读 computed)
readonly user = computed(() => this._user());
readonly loading = computed(() => t
his._loading());
readonly error = computed(() => this._error());
readonly isAuthenticated = computed(() => this._user() !== null);
readonly displayName = computed(() => this._user()?.name ?? "Guest");
// Actions
async loadUser(id: string) {
this._loading.set(true);
this._error.set(null);
try {
const user = await fetch(/api/users/${id}).then((r) => r.json());
this._user.set(user);
} catch (e) {
this._error.set("Failed to load user");
} finally {
this._loading.set(false);
}
}
updateUser(updates: Partial<User>) {
this._user.update((user) => (user ? { ...user, ...updates } : null));
}
logout() {
this._user.set(null);
this._error.set(null);
}
}
### 模式 3:SignalStore (NgRx Signals)// stores/products.store.ts
import {
signalStore,
withState,
withMethods,
withComputed,
patchState,
} from "@ngrx/signals";
import { inject } from "@angular/core";
import { ProductService } from "./product.service";
interface ProductState {
products: Product[];
loading: boolean;
filter: string;
}
const initialState: ProductState = {
products: [],
loading: false,
filter: "",
};
export const ProductStore = signalStore(
{ providedIn: "root" },
withState(initialState),
withComputed((store) => ({
filteredProducts: computed(() => {
const filter = store.filter().toLowerCase();
return store
.products()
.filter((p) => p.name.toLowerCase().includes(filter));
}),
totalCount: computed(() => store.products().length),
})),
withMethods((store, productService = inject(ProductService)) => ({
async loadProducts() {
patchState(store, { loading: true });
try {
const products = await productService.getAll();
patchState(store, { products, loading: false });
} catch {
patchState(store, { loading: false });
}
},
setFilter(filter: string) {
patchState(store, { filter });
},
addProduct(product: Product) {
patchState(store, ({ products }) => ({
products: [...products, product],
}));
},
})),
);
// 使用示例
@Component({
template: ,
<input (input)="store.setFilter($event.target.value)" />
@if (store.loading()) {
<app-spinner />
} @else {
@for (product of store.filteredProducts(); track product.id) {
<app-product-card [product]="product" />
}
}
})
export class ProductListComponent {
store = inject(ProductStore);
ngOnInit() {
this.store.loadProducts();
}
}
---
NgRx Store (全局状态管理)
配置
export interface AppState {
user: UserState;
cart: CartState;
}
export const reducers: ActionReducerMap<AppState> = {
user: userReducer,
cart: cartReducer,
};
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideStore(reducers),
provideEffects([UserEffects, CartEffects]),
provideStoreDevtools({ maxAge: 25 }),
],
});
### 功能切片模式 (Feature Slice Pattern)// store/user/user.actions.ts
import { createActionGroup, props, emptyProps } from "@ngrx/store";
export const UserActions = createActionGroup({
source: "User",
events: {
"Load User": props<{ userId: string }>(),
"Load User Success": props<{ user: User }>(),
"Load User Failure": props<{ error: string }>(),
"Update User": props<{ updates: Partial<User
> }>(),
Logout: emptyProps(),
},
});
// store/user/user.reducer.ts
import { createReducer, on } from "@ngrx/store";
import { UserActions } from "./user.actions";
export interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
loading: false,
error: null,
};
export const userReducer = createReducer(
initialState,
on(UserActions.loadUser, (state) => ({
...state,
loading: true,
error: null,
})),
on(UserActions.loadUserSuccess, (state, { user }) => ({
...state,
user,
loading: false,
})),
on(UserActions.loadUserFailure, (state, { error }) => ({
...state,
loading: false,
error,
})),
on(UserActions.logout, () => initialState),
);
// store/user/user.selectors.ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { UserState } from "./user.reducer";
export const selectUserState = createFeatureSelector<UserState>("user");
export const selectUser = createSelector(
selectUserState,
(state) => state.user,
);
export const selectUserLoading = createSelector(
selectUserState,
(state) => state.loading,
);
export const selectIsAuthenticated = createSelector(
selectUser,
(user) => user !== null,
);
// store/user/user.effects.ts
import { Injectable, inject } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { switchMap, map, catchError, of } from "rxjs";
@Injectable()
export class UserEffects {
private actions$ = inject(Actions);
private userService = inject(UserService);
loadUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUser),
switchMap(({ userId }) =>
this.userService.getUser(userId).pipe(
map((user) => UserActions.loadUserSuccess({ user })),
catchError((error) =>
of(UserActions.loadUserFailure({ error: error.message })),
),
),
),
),
);
}
### 组件使用@Component({
template:
@if (loading()) {
<app-spinner />
} @else if (user(); as user) {
<h1>Welcome, {{ user.name }}</h1>
<button (click)="logout()">Logout</button>
}
,})
export class HeaderComponent {
private store = inject(Store);
user = this.store.selectSignal(selectUser);
loading = this.store.selectSignal(selectUserLoading);
logout() {
this.store.dispatch(UserActions.logout());
}
}
---
基于 RxJS 的模式
Component Store (局部功能状态)
interface TodoState {
todos: Todo[];
loading: boolean;
}
@Injectable()
export class TodoStore extends ComponentStore<TodoState> {
constructor(private todoService: TodoService) {
super({ todos: [], loading: false });
}
// Selectors (选择器)
readonly todos$ = this.select((state) => state.todos);
readonly loading$ = this.select((state) => state.loading);
readonly completedCount$ = this.select(
this.todos$,
(todos) => todos.filter((t) => t.completed).length,
);
// Updaters (更新器)
readonly addTodo = this.updater((state, todo: Todo) => ({
...state,
todos: [...state.todos, todo],
}));
readonly toggleTodo = this.updater((state, id: string) => ({
...
state,
todos: state.todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t,
),
}));
// Effects
readonly loadTodos = this.effect<void>((trigger$) =>
trigger$.pipe(
tap(() => this.patchState({ loading: true })),
switchMap(() =>
this.todoService.getAll().pipe(
tap({
next: (todos) => this.patchState({ todos, loading: false }),
error: () => this.patchState({ loading: false }),
}),
catchError(() => EMPTY),
),
),
),
);
}
---
使用 Signals 管理服务端状态
HTTP + Signals 模式
// services/api.service.ts
import { Injectable, signal, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";
interface ApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
@Injectable({ providedIn: "root" })
export class ProductApiService {
private http = inject(HttpClient);
private _state = signal<ApiState<Product[]>>({
data: null,
loading: false,
error: null,
});
readonly products = computed(() => this._state().data ?? []);
readonly loading = computed(() => this._state().loading);
readonly error = computed(() => this._state().error);
async fetchProducts(): Promise<void> {
this._state.update((s) => ({ ...s, loading: true, error: null }));
try {
const data = await firstValueFrom(
this.http.get<Product[]>("/api/products"),
);
this._state.update((s) => ({ ...s, data, loading: false }));
} catch (e) {
this._state.update((s) => ({
...s,
loading: false,
error: "Failed to fetch products",
}));
}
}
// 乐观更新
async deleteProduct(id: string): Promise<void> {
const previousData = this._state().data;
// 乐观删除
this._state.update((s) => ({
...s,
data: s.data?.filter((p) => p.id !== id) ?? null,
}));
try {
await firstValueFrom(this.http.delete(/api/products/${id}));
} catch {
// 错误时回滚
this._state.update((s) => ({ ...s, data: previousData }));
}
}
}
---
最佳实践
推荐做法 (Do's)
| 实践 | 原因 |
| ---------------------------------- | ---------------------------------- |
| 使用 Signals 管理本地状态 | 简单、响应式,无需手动订阅 |
| 使用 computed() 处理派生数据 | 自动更新,且具有缓存机制 |
| 将状态与功能模块共置 | 更易于维护 |
| 复杂流程使用 NgRx | 拥有 Action、Effect 和 DevTools 支持 |
| 优先使用 inject() 而非构造函数 | 代码更简洁,适用于工厂函数 |
避免做法 (Don'ts)
| 反模式 | 替代方案 |
| --------------------------------- | ----------------------------------------------------- |
| 存储派生数据 | 使用 computed() |
| 直接修改 Signal 的值 | 使用 set() 或 update() |
| 过度全局化状态 | 尽可能保持在本地 |
| 混乱地混合使用 RxJS 和 Signals | 确定主导方案,通过 toSignal/toObservable 桥接 |
| 在组件中订阅状态 | 在模板中直接使用 Signals |
---
迁移路径
从 BehaviorS
迁移至 Signals// 之前:基于 RxJS
@Injectable({ providedIn: "root" })
export class OldUserService {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
setUser(user: User) {
this.userSubject.next(user);
}
}
// 之后:基于 Signal
@Injectable({ providedIn: "root" })
export class UserService {
private _user = signal<User | null>(null);
readonly user = this._user.asReadonly();
setUser(user: User) {
this._user.set(user);
}
}
Signals 与 RxJS 的桥接
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable → Signal
@Component({...})
export class ExampleComponent {
private route = inject(ActivatedRoute);
// 将 Observable 转换为 Signal
userId = toSignal(
this.route.params.pipe(map(p => p['id'])),
{ initialValue: '' }
);
}
// Signal → Observable
export class DataService {
private filter = signal('');
// 将 Signal 转换为 Observable
filter$ = toObservable(this.filter);
filteredData$ = this.filter$.pipe(
debounceTime(300),
switchMap(filter => this.http.get(/api/data?q=${filter}))
);
}
---
相关资源
局限性
- 仅在任务明确符合上述范围时使用此技能。
- 不要将输出结果视为环境特定验证、测试或专家评审的替代方案。
- 如果缺少必要的输入、权限、安全边界或验收标准,请停止并请求澄清。