Angular 状态管理

angular-state-management
分类通用
作者Agentic Awesome Skills 社区
许可MIT
评分4.40/5
使用15.4K

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 |

选择标准

code
小型应用,简单状态 $\rightarrow$ Signal Services
中型应用,中等状态 $\rightarrow$ Component Stores
大型应用,复杂状态 $\rightarrow$ NgRx Store
频繁服务器交互 $\rightarrow$ NgRx Query + Signal Services
实时更新 $\rightarrow$ RxAngular + Signals

---

快速上手:基于 Signal 的状态

模式 1:简单 Signal 服务

typescript
// 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

typescript
// 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);
}
}

code
### 模式 3:SignalStore (NgRx Signals)
typescript
// 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();
}
}

code
---

NgRx Store (全局状态管理)

配置

typescript // store/app.state.ts import { ActionReducerMap } from "@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 }),
],
});

code
### 功能切片模式 (Feature Slice Pattern)
typescript
// 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(),
},
});

code
typescript
// 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),
);

code
typescript
// 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,
);

code
typescript
// 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 })),
),
),
),
),
);
}

code
### 组件使用
typescript
@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());
}
}

code
---

基于 RxJS 的模式

Component Store (局部功能状态)

typescript // stores/todo.store.ts import { Injectable } from "@angular/core"; import { ComponentStore } from "@ngrx/component-store"; import { switchMap, tap, catchError, EMPTY } from "rxjs";

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) => ({
...

code
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 模式

typescript
// 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
typescript
// 之前:基于 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 的桥接

typescript
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}))
);
}

---

相关资源

局限性

  • 仅在任务明确符合上述范围时使用此技能。
  • 不要将输出结果视为环境特定验证、测试或专家评审的替代方案。
  • 如果缺少必要的输入、权限、安全边界或验收标准,请停止并请求澄清。