mcpField
N
ngrx-signals

ngrx-signals

github.com/danielsogl开发542 次安装7 周前更新
代码仓库

关于 ngrx-signals

ngrx-signals 是用于采用 `@ngrx/signals` 编写符合惯例、类型安全的 Angular 状态管理代码的技能,涵盖 Signal Store 组合、异步模式、实体集合、架构和测试。

主要功能

  1. 01类型化的 Signal Store 状态与更新
  2. 02可组合的 Store 功能
  3. 03异步方法模式指导
  4. 04实体集合模式
  5. 05通过公开 API 进行测试

适用场景

  1. 01构建 Angular 功能或页面状态
  2. 02重构基于 BehaviorSubject 的服务
  3. 03实现实体 CRUD 与乐观更新
  4. 04创建可复用的 Signal Store 功能

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/danielsogl/skills --skill ngrx-signals

安装后在 Claude Code 中输入/ngrx-signals确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
evals/
evals.json
evals/checks/
tsc.sh
references/
api-reference.md
custom-features.md
entities.md
patterns.md
testing.md

详细介绍

NgRx Signal Store

面向 Angular 的现代、轻量级、基于信号的状态管理方案。此技能可生成符合惯用写法、类型安全、架构良好的 @ngrx/signals 代码。

何时使用此技能

出现以下任一情况时触发:

  • 用户提到这些 API:signalStore、signalStoreFeature、withState、withMethods、withComputed、withLinkedState、withHooks、withProps、withFeature、withEntities、rxMethod、signalMethod、patchState、getState、watchState、signalState 或 @ngrx/signals/events / @ngrx/signals/testing。
  • 用户希望将使用 BehaviorSubject/Subject 的服务或带有可变字段的类重构为 Signal Store。
  • 用户在 Angular 中询问功能/页面状态,并且提到 Angular Signals,或询问“现代”的实现方式。
  • 用户想要自定义 Store 功能(如 withRequestStatus、withSelectedEntity、withLogger 等),或询问如何组合功能。
  • 用户在 Angular 功能中询问乐观更新、请求状态、实体集合,或如何通过 HTTP API 执行 CRUD。

不要用于传统的 @ngrx/store(actions、reducers、effects、单独文件中的 selectors)、不涉及 Signal Store 的 Redux DevTools 配置,或纯 RxJS 管道问题。

心智模型

Signal Store 是由多个功能组合而成的树。每个功能都可以提供状态、计算信号、方法、属性和生命周期钩子。组合发生在定义时:

export const FooStore = signalStore(
  { providedIn: 'root' },     // optional Angular provider config
  withState(initialState),    // declares state slices (signals)
  withComputed(...),          // derived signals
  withProps(...),             // non-reactive deps & values (often via inject())
  withMethods(...),           // actions / mutations
  withHooks({ onInit, onDestroy })
);

每位 Signal Store 编写者都需要牢记以下三条规则:

  1. 状态更新必须通过 patchState(store, updater) 完成。 不存在直接变更。Updater 可以是部分对象、函数 (state) => Partial<State>,或返回上述任一种结果的自定义 updater。
  2. Store 是 Angular 提供者。 使用 inject(Store) 或 TestBed.inject(Store) 注入。可在根级别、路由上或组件上提供(providers: [Store]),以管理组件级状态。
  3. 优先组合,而非继承。 通过 signalStoreFeature(...) 构建自定义功能以实现复用,而不是扩展类。

标准脚手架(可作为起点)

对于 80% 的功能型 Store,都可以采用以下结构。复制并调整,然后查阅参考资料了解不明显之处。

import { computed, inject } from '@angular/core';
import {
  patchState, signalStore, withComputed, withMethods, withState,
} from '@ngrx/signals';
import { Foo, FooApi } from './foo.api';
import {
  setError, setFulfilled, setPending, withRequestStatus,
} from './with-request-status';

type FooState = {
  items: Foo[];
  filter: string;
};

const initialState: FooState = {
  items: [],
  filter: '',
};

export const FooStore = signalStore(
  { providedIn: 'root' },
  withState<FooState>(initialState),
  withRequestStatus(),
  withComputed(({ items, filter }) => ({
    filteredItems: computed(() =>
      items().filter((i) => i.name.includes(filter()))
    ),
    count: computed(() => items().length),
  })),
  withMethods((store, api = inject(FooApi)) => ({
    setFilter(filter: string): void {
      patchState(store, { filter });
    },
    async loadAll(): Promise<void> {
      patchState(store, setPending());
      try {
        const items = await api.getAll();
        patchState(store, { items }, setFulfilled());
      } catch (e) {
        patchState(store, setError(e instanceof Error ? e.message : 'Failed'));
      }
    },
  })),
);

对于 ID 稳定的集合,将状态中的 items: T[] 替换为 withEntities<T>()。参见 references/entities.md。

工作流

  1. 确定作用范围。 仅限组件?使用 providers: [Store]。全应用?使用 { providedIn: 'root' }。按路由提供?在路由上提供。不要将路由级状态放在 root 中。
  2. 确定 Store/服务的职责划分。 状态、计算和流程编排属于 Store。HTTP / 持久化 / WebSocket / IndexedDB 属于单独的服务,由 Store 调用 inject() 注入。两部分都可以独立测试;替换后端时无需修改 Store。
  3. 显式声明状态类型。 使用 withState<FooState>(initialState)——不要只依赖类型推断。后续调用 patchState 时需要该类型来检查拼写错误。
  4. 组合功能。 将请求状态、实体管理和日志记录提取到 signalStoreFeature(...),以便在多个 Store 中复用。
  5. 选择合适的异步工具。
    • 只需等待单次调用时,使用 async 方法。
    • 需要 RxJS 运算符(debounceTime、switchMap、重试、取消)时,使用 rxMethod<Input>(pipe(...))。
    • 输入为信号/值且方法体同步时,使用 signalMethod<Input>((value) => ...)——无需 RxJS 开销。
    • 要向其中任一方法传入信号或计算函数?请在注入上下文中调用,或传入 { injector }——在注入上下文之外调用已弃用。
  6. 仅通过公开 API 进行测试。 使用 TestBed.inject(Store),通过 provide: 模拟注入的服务,并断言信号和方法产生的效果。使用 @ngrx/signals/testing 中的 patchState(unprotected(store), ...) 为受保护的状态设置初始值。

错误 → 正确(最重要的更正)

这些是最常出现的错误。生成代码时,请始终牢记这些模式。

1. updateEntity — 对依赖状态的更新使用函数形式
// ❌ Wrong: read entityMap manually then patch with a partial — race-prone
//          and leaks store internals into the method body.
const current = store.entityMap()[id];
patchState(store, updateEntity({ id, changes: { completed: !current.completed } }));

// ✅ Right: function-form changes — atomic, no manual lookup.
patchState(store, updateEntity({
  id,
  changes: (todo) => ({ completed: !todo.completed }),
}));
2. removeEntities — 使用谓词形式,而不是 filter+map
// ❌ Wrong: pulls everything into the method body, fragile if signal shape changes.
const ids = store.entities().filter((t) => t.completed).map((t) => t.id);
patchState(store, removeEntities(ids));

// ✅ Right: predicate is evaluated against each entity by the updater.
patchState(store, removeEntities((todo) => todo.completed));
3. 乐观更新 — 使用 getState 创建快照,出错时还原
// ❌ Wrong: no rollback path — the UI lies if the API call fails.
patchState(store, addEntity(item));
await api.add(item);

// ✅ Right: snapshot, patch optimistically, restore on failure.
const previous = getState(store);
patchState(store, addEntity(item));
try {
  await api.add(item);
} catch (e) {
  patchState(store, { entityMap: previous.entityMap, ids: previous.ids });
  throw e;
}
4. 请求状态 — 使用独立的更新器,而不是绑定到 store 的方法
// ❌ Wrong: reaching into the store from the feature, can't compose into patchState calls.
withMethods((store) => ({ setPending() { patchState(store, { requestStatus: 'pending' }); } }))

// ✅ Right: standalone updater functions that return Partial<State>.
//          Compose with other updaters in a single patchState call.
export function setPending(): RequestStatusState { return { requestStatus: 'pending' }; }
patchState(store, setAllEntities(items), setFulfilled());
5. 带前置条件的自定义特性 — 使用类型化重载
// ❌ Wrong: bare signalStoreFeature — no compile error if used without withEntities.
export function withSelectedEntity<T>() {
  return signalStoreFeature(withState({ selectedId: null }), /* ... */);
}

// ✅ Right: declare prerequisite via { state: type<EntityState<T>>() }.
//          Now misuse is a compile error.
export function withSelectedEntity<T extends { id: EntityId }>() {
  return signalStoreFeature(
    { state: type<EntityState<T>>() },
    withState<{ selectedId: EntityId | null }>({ selectedId: null }),
    withComputed(({ selectedId, entityMap }) => ({
      selectedEntity: computed(() => {
        const id = selectedId();
        return id == null ? null : (entityMap()[id] ?? null);
      }),
    })),
    withMethods((store) => ({
      select(id: EntityId): void { patchState(store, { selectedId: id }); },
      clearSelection(): void { patchState(store, { selectedId: null }); },
    })),
  );
}
6. 重构 BehaviorSubject — 各部分的对应关系
重构前重构后
BehaviorSubject<T> 字段withState<FooState>(...) 中的状态切片
.next(...)patchState(store, ...)
.value信号 getter store.foo()(或 getState(store))
combineLatest / map 派生的可观察对象返回 computed(...) 的 withComputed(...)
服务上的 HTTP 方法由 store 注入的新 FooApi 服务
组件通过 | async 订阅读取信号:{{ store.foo() }}(不使用 async 管道)

输出约定

生成代码时:

  • 始终使用 TypeScript,明确声明状态类型,并明确从 @ngrx/signals / @ngrx/signals/entities / @ngrx/signals/rxjs-interop 导入内容。
  • 始终使用 patchState 进行变更,绝不直接给信号赋值。
  • 始终将 HTTP 逻辑放在通过 inject(SomeApi) 注入的服务中;store 不导入 HttpClient。
  • 优先使用命名且类型明确的更新器函数(setPending()、setError(msg)),尤其是在多个地方执行相同更新时。
  • 优先使用 withEntities<T>(),而不是手写 T[] 数组(当条目具有稳定的 id 时)。
  • 优先使用 signalMethod,而不是 rxMethod(无需 RxJS 运算符时)。
  • 仅在原因不明显时添加注释。不要赘述显而易见的内容。

模式 → 参考文件速查表

阅读与任务匹配的一份参考文件。大多数任务只需阅读一份。

任务阅读
普通特性 store、计算属性、方法references/api-reference.md
rxMethod / signalMethod / RxJS 桥接references/api-reference.md
withLinkedState、watchState、事件插件、资源扩展references/api-reference.md
实体集合(使用 ID 执行 CRUD)references/entities.md
自定义可复用特性(withXxx)references/custom-features.md
架构:作用域、乐观更新、BS 重构references/patterns.md
测试:TestBed、模拟对象、signalMethod 测试references/testing.md

如果任务涉及多种模式(例如,“带有乐观更新和请求状态的实体 store”),通常只需使用上方的框架代码和 entities.md。

需要指出的反模式

如果请求或现有代码中出现以下任何情况,请指出:

  • 混淆 @ngrx/store 和 @ngrx/signals。 这是不同的包。createReducer/createEffect/@Effect 属于经典 NgRx,而非 Signal Store。
  • 在 store 中处理 HTTP。 将其移至服务。store 不应导入 HttpClient。
  • 跨 store 导入。 store 绝不注入其他 store。应将状态提升到更高层,通过 feature 组合,或共享服务。
  • 为实体数据手动维护 T[] 数组。 如果 ID 稳定,请使用 withEntities<T>()。
  • 从外部直接修改 signal。 store.count.set(...) 会破坏封装。应公开一个方法。
  • 对简单的同步逻辑使用 rxMethod。 请使用 signalMethod 或普通方法。
  • 在另一个切片发生变化时重新同步某个切片(例如,在 items 变化时通过 effect 或方法重置 selectedId)。请使用 withLinkedState。
  • 状态未指定类型。 withState({ count: 0 }) 可以正常工作;withState<CounterState>({ count: 0 }) 可确保后续补丁具有类型安全性。
  • 使用过时的 API 名称。 withEffects(events 插件)现在是 withEventHandlers;实体的 idKey 现在通过 entityConfig 使用 selectId;rxMethod(...).unsubscribe() 现在是 .destroy()。

评论

支持 Markdown