feature-systems-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Feature Systems Guide

领域功能系统指南

A "system" is a self-contained, domain-driven module that owns everything related to one domain: its API calls, query layer, hooks, components, and public API. Systems live under a
systems/<domain>/
directory.
Read
references/directory-layout.md
for the full directory structure and naming conventions. Read
references/patterns.md
for annotated implementation patterns per layer.
“系统”是一个独立的、领域驱动的模块,负责管理与某一领域相关的所有内容:API调用、查询层、钩子、组件和公共API。系统存放在
systems/<domain>/
目录下。
阅读
references/directory-layout.md
了解完整的目录结构和命名约定。 阅读
references/patterns.md
了解各层的带注释实现模式。

Quick Reference

快速参考

Mandatory Companion Skills

必备配套技能

Activate alongside this skill — systems span multiple technical domains:
SituationActivate
Any hook or component
react
+
tanstack-query
Data fetching/caching
tanstack-query
Mutations
tanstack-query
XState store
xstate
Utility functions
es-toolkit
Writing/fixing tests
testing-boss
+
vitest
Bug fix
systematic-debugging
+
no-workarounds
与本技能一同激活——系统涵盖多个技术领域:
场景需激活的技能
任何钩子或组件
react
+
tanstack-query
数据获取/缓存
tanstack-query
数据突变
tanstack-query
XState存储
xstate
工具函数
es-toolkit
编写/修复测试
testing-boss
+
vitest
修复bug
systematic-debugging
+
no-workarounds

System Directory at a Glance

系统目录概览

systems/<domain>/
├── index.ts               # Public API barrel — required for every system
├── types.ts               # TypeScript types for this domain
├── adapters/              # API service layer (HTTP calls, error types)
│   └── <domain>-api.ts
├── lib/                   # Pure utilities, schemas, constants, query keys
│   ├── query-keys.ts      # TanStack Query key factory
│   ├── query-options.ts   # Reusable queryOptions / mutationOptions
│   ├── <domain>-schemas.ts
│   └── constants.ts
├── hooks/                 # React hooks (queries, mutations, view-models)
│   ├── __tests__/
│   ├── use-<action>.ts    # Query hooks
│   ├── use-create-<entity>.ts  # Mutation hooks
│   ├── use-update-<entity>.ts
│   ├── use-delete-<entity>.ts
│   └── use-<domain>-view-model.ts
├── contexts/              # React contexts + providers
│   └── <domain>-context.tsx
├── stores/                # XState stores (complex async state machines)
│   └── <domain>-store.ts
├── components/            # React UI components
│   ├── stories/
│   └── index.ts
└── guards/                # Route guards / access checks
systems/<domain>/
├── index.ts               # 公共API桶——每个系统都必需
├── types.ts               # 该领域的TypeScript类型
├── adapters/              # API服务层(HTTP调用、错误类型)
│   └── <domain>-api.ts
├── lib/                   # 纯工具函数、Schema、常量、查询键
│   ├── query-keys.ts      # TanStack Query键工厂
│   ├── query-options.ts   # 可复用的queryOptions / mutationOptions
│   ├── <domain>-schemas.ts
│   └── constants.ts
├── hooks/                 # React钩子(查询、突变、视图模型)
│   ├── __tests__/
│   ├── use-<action>.ts    # 查询钩子
│   ├── use-create-<entity>.ts  # 突变钩子
│   ├── use-update-<entity>.ts
│   ├── use-delete-<entity>.ts
│   └── use-<domain>-view-model.ts
├── contexts/              # React contexts + 提供者
│   └── <domain>-context.tsx
├── stores/                # XState存储(复杂异步状态机)
│   └── <domain>-store.ts
├── components/            # React UI组件
│   ├── stories/
│   └── index.ts
└── guards/                # 路由守卫/权限检查

Step-by-Step: Creating a New System

分步指南:创建新系统

Step 1 — Define types.ts

步骤1 — 定义types.ts

  • Export clean domain types; never expose raw API response shapes.
  • Derive from the project's API contract types when available.
  • Document complex aggregated types with JSDoc explaining derivation rules and invariants.
  • 导出清晰的领域类型;绝不要暴露原始API响应结构。
  • 若项目有API契约类型,请基于这些类型派生。
  • 为复杂的聚合类型添加JSDoc注释,说明派生规则和不变量。

Step 2 — Build the API service layer

步骤2 — 构建API服务层

  • Create
    adapters/<domain>-api.ts
    .
  • Use the project's HTTP client for API calls.
  • Export a single namespace object:
    export const <domain>Api = { list, create, update, delete }
    .
  • Export a typed error class:
    export class <Domain>ApiError extends Error { ... }
    .
  • Accept
    signal?: AbortSignal
    on every function to support query cancellation.
  • Keep all internal helpers (error extraction, response normalization) private to the module.
  • 创建
    adapters/<domain>-api.ts
  • 使用项目的HTTP客户端进行API调用。
  • 导出单个命名空间对象:
    export const <domain>Api = { list, create, update, delete }
  • 导出一个带类型的错误类:
    export class <Domain>ApiError extends Error { ... }
  • 每个函数都接受
    signal?: AbortSignal
    以支持查询取消。
  • 所有内部辅助函数(错误提取、响应规范化)都保持模块私有。

Step 3 — Add lib/query-keys.ts

步骤3 — 添加lib/query-keys.ts

ts
export const <domain>Keys = {
  all: ["<domain>"] as const,
  lists: () => [...<domain>Keys.all, "list"] as const,
  list: (scopeId: string | null) => [...<domain>Keys.lists(), scopeId] as const,
  details: () => [...<domain>Keys.all, "detail"] as const,
  detail: (id: string) => [...<domain>Keys.details(), id] as const,
};
  • Use hierarchical key structure for granular invalidation.
  • Scope keys with any identifier (userId, orgId, etc.) that isolates the cache correctly.
  • Use
    as const
    on every key tuple.
ts
export const <domain>Keys = {
  all: ["<domain>"] as const,
  lists: () => [...<domain>Keys.all, "list"] as const,
  list: (scopeId: string | null) => [...<domain>Keys.lists(), scopeId] as const,
  details: () => [...<domain>Keys.all, "detail"] as const,
  detail: (id: string) => [...<domain>Keys.details(), id] as const,
};
  • 使用层级键结构实现细粒度缓存失效。
  • 使用任何标识符(userId、orgId等)限定键的范围,确保缓存隔离正确。
  • 每个键元组都使用
    as const

Step 4 — Add lib/query-options.ts

步骤4 — 添加lib/query-options.ts

ts
import { queryOptions } from "@tanstack/react-query";
import { <domain>Api } from "../adapters/<domain>-api";
import { <domain>Keys } from "./query-keys";

export function <domain>ListOptions(scopeId: string | null) {
  return queryOptions({
    queryKey: <domain>Keys.list(scopeId),
    queryFn: ({ signal }) => <domain>Api.list(scopeId!, signal),
    staleTime: 60_000,
    enabled: Boolean(scopeId),
  });
}

export function <domain>DetailOptions(id: string) {
  return queryOptions({
    queryKey: <domain>Keys.detail(id),
    queryFn: ({ signal }) => <domain>Api.get(id, signal),
    enabled: Boolean(id),
  });
}
  • Co-locate
    queryKey
    and
    queryFn
    via
    queryOptions
    for type safety and reuse.
  • Export each option factory for use in hooks, prefetching, and route loaders.
  • Always pass
    signal
    from the query context through to the API layer.
ts
import { queryOptions } from "@tanstack/react-query";
import { <domain>Api } from "../adapters/<domain>-api";
import { <domain>Keys } from "./query-keys";

export function <domain>ListOptions(scopeId: string | null) {
  return queryOptions({
    queryKey: <domain>Keys.list(scopeId),
    queryFn: ({ signal }) => <domain>Api.list(scopeId!, signal),
    staleTime: 60_000,
    enabled: Boolean(scopeId),
  });
}

export function <domain>DetailOptions(id: string) {
  return queryOptions({
    queryKey: <domain>Keys.detail(id),
    queryFn: ({ signal }) => <domain>Api.get(id, signal),
    enabled: Boolean(id),
  });
}
  • 通过
    queryOptions
    queryKey
    queryFn
    放在一起,确保类型安全和可复用性。
  • 导出每个选项工厂,供钩子、预取和路由加载器使用。
  • 务必将查询上下文的
    signal
    传递到API层。

Step 5 — Write hooks

步骤5 — 编写钩子

  • Query hooks: Wrap
    useQuery
    with the
    queryOptions
    factories; accept a scope ID + optional
    { enabled? }
    .
  • Mutation hooks: Use
    useMutation
    with proper
    onMutate
    /
    onError
    /
    onSettled
    callbacks for optimistic updates.
  • View-model hooks: Compose multiple hooks for a page/shell component; return a flat object.
  • Place tests in
    hooks/__tests__/
    or co-locate as
    use-xxx.test.tsx
    .
Read
references/patterns.md
for complete mutation and optimistic update patterns.
  • 查询钩子:用
    queryOptions
    工厂包装
    useQuery
    ;接受范围ID + 可选的
    { enabled? }
  • 突变钩子:使用
    useMutation
    并配合适当的
    onMutate
    /
    onError
    /
    onSettled
    回调实现乐观更新。
  • 视图模型钩子:为页面/外壳组件组合多个钩子;返回扁平对象。
  • 测试文件放在
    hooks/__tests__/
    中,或与钩子文件共存为
    use-xxx.test.tsx
阅读
references/patterns.md
了解完整的突变和乐观更新模式。

Step 6 — (Optional) Add context

步骤6 —(可选)添加Context

Create
contexts/<domain>-context.tsx
when query data or combined state must be shared across a component subtree without prop-drilling.
ts
// Always nullable context — consumer hook throws if used outside provider
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);
  • Export the context, provider component, and re-export consumer hooks from the same file.
  • For performance-sensitive trees, split into Core / UI / Operations sub-contexts.
当查询数据或组合状态需要在组件子树中共享而无需通过props传递时,创建
contexts/<domain>-context.tsx
ts
// Context始终可为null——若在提供者外部使用,消费钩子会抛出错误
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);
  • 导出Context、提供者组件,并从同一文件中重新导出消费钩子。
  • 对于性能敏感的组件树,可拆分为Core/UI/Operations子Context。

Step 7 — (Optional) Add an XState store

步骤7 —(可选)添加XState存储

Create
stores/<domain>-store.ts
for complex async state machines (multi-step flows, polling, event emission).
ts
export const <domain>Store = createStore({
  context: { ... } as <Domain>Context,
  emits: { ... },
  on: {
    someEvent: (context, event, enqueue) => {
      enqueue.effect(async () => { ... });
      return { ...context, isLoading: true };
    },
  },
});
为复杂的异步状态机(多步骤流程、轮询、事件发射)创建
stores/<domain>-store.ts
ts
export const <domain>Store = createStore({
  context: { ... } as <Domain>Context,
  emits: { ... },
  on: {
    someEvent: (context, event, enqueue) => {
      enqueue.effect(async () => { ... });
      return { ...context, isLoading: true };
    },
  },
});

Step 8 — Wire up index.ts

步骤8 — 连接index.ts

Organize the barrel with labeled sections and explicit named exports:
ts
// Types
export type { <Domain>Type } from "./types";

// Hooks
export { use<Domain>List, use<Domain>Detail } from "./hooks";
export { useCreate<Domain>, useUpdate<Domain>, useDelete<Domain> } from "./hooks";

// Components
export { <Domain>Component } from "./components";

// Utilities
export { <domain>HelperFn } from "./lib/<domain>-utils";

// Query Keys & Options
export { <domain>Keys } from "./lib/query-keys";
export { <domain>ListOptions, <domain>DetailOptions } from "./lib/query-options";

// API
export { <domain>Api, <Domain>ApiError } from "./adapters/<domain>-api";
使用带标签的章节和显式命名导出组织桶文件:
ts
// 类型
export type { <Domain>Type } from "./types";

// 钩子
export { use<Domain>List, use<Domain>Detail } from "./hooks";
export { useCreate<Domain>, useUpdate<Domain>, useDelete<Domain> } from "./hooks";

// 组件
export { <Domain>Component } from "./components";

// 工具函数
export { <domain>HelperFn } from "./lib/<domain>-utils";

// 查询键与选项
export { <domain>Keys } from "./lib/query-keys";
export { <domain>ListOptions, <domain>DetailOptions } from "./lib/query-options";

// API
export { <domain>Api, <Domain>ApiError } from "./adapters/<domain>-api";

Critical Rules

关键规则

  1. Use
    queryOptions
    for co-location.
    Co-locate
    queryKey
    and
    queryFn
    in reusable option factories. Never scatter the same query key across multiple files.
  2. Unidirectional dependency flow.
    adapters -> lib -> hooks -> components
    . Adapters never import from hooks or components.
  3. Scope query keys. Any query depending on an authenticated scope (user, org, tenant) must include that scope ID in its key to prevent stale cross-scope data.
  4. Typed errors in the API layer. Never throw raw errors from adapters. Use a typed error class so consumers can distinguish error types without inspecting message strings.
  5. AbortSignal propagation. Pass
    signal
    from the
    queryFn
    context through to every API call for proper query cancellation.
  6. Always invalidate after mutations. Use
    queryClient.invalidateQueries
    in
    onSettled
    to ensure eventual consistency with the server.
  7. Optimistic updates require rollback. When using cache-based optimistic updates, snapshot previous data in
    onMutate
    and restore in
    onError
    .
  8. Cancel outgoing queries before optimistic updates. Call
    queryClient.cancelQueries
    in
    onMutate
    to prevent refetches from overwriting optimistic state.
  9. Zod schemas in lib/. Place all Zod schemas in
    lib/<domain>-schemas.ts
    for runtime validation at API boundaries.
  1. 使用
    queryOptions
    实现代码共置
    :将
    queryKey
    queryFn
    放在可复用的选项工厂中。绝不要在多个文件中分散相同的查询键。
  2. 单向依赖流
    adapters -> lib -> hooks -> components
    。Adapters绝不能从hooks或components导入。
  3. 限定查询键的范围:任何依赖于认证范围(用户、组织、租户)的查询必须在其键中包含该范围ID,以防止跨范围的 stale 数据。
  4. API层使用带类型的错误:绝不要从adapters抛出原始错误。使用带类型的错误类,以便消费者无需检查消息字符串即可区分错误类型。
  5. 传递AbortSignal:将
    queryFn
    上下文的
    signal
    传递到每个API调用,以实现正确的查询取消。
  6. 突变后始终失效缓存:在
    onSettled
    中使用
    queryClient.invalidateQueries
    确保与服务器最终一致。
  7. 乐观更新需要回滚:当使用基于缓存的乐观更新时,在
    onMutate
    中快照先前的数据,并在
    onError
    中恢复。
  8. 乐观更新前取消正在进行的查询:在
    onMutate
    中调用
    queryClient.cancelQueries
    ,防止重新获取覆盖乐观状态。
  9. Zod Schema放在lib/中:所有Zod Schema都放在
    lib/<domain>-schemas.ts
    中,用于API边界的运行时验证。

Error Handling

错误处理

  • API layer throws typed error: TanStack Query catches and exposes it via
    query.error
    .
  • Mutation fails with optimistic update:
    onError
    callback rolls back the cache to the snapshot from
    onMutate
    , then
    onSettled
    invalidates to refetch fresh data.
  • Stale cross-scope data: Add the scope ID to the query key and verify that
    enabled
    guards check
    Boolean(scopeId)
    .
  • Query cancellation on unmount: TanStack Query automatically cancels in-flight queries via the
    signal
    when a component unmounts — ensure
    signal
    is propagated to the API layer.
  • API层抛出带类型的错误:TanStack Query会捕获错误并通过
    query.error
    暴露。
  • 突变失败且已执行乐观更新
    onError
    回调将缓存回滚到
    onMutate
    中的快照,然后
    onSettled
    失效缓存以重新获取最新数据。
  • 跨范围的stale数据:将范围ID添加到查询键中,并验证
    enabled
    守卫是否检查
    Boolean(scopeId)
  • 组件卸载时取消查询:当组件卸载时,TanStack Query会通过
    signal
    自动取消正在进行的查询——确保
    signal
    已传递到API层。

Detailed References

详细参考

  • references/directory-layout.md
    — Full directory structure, file naming, and barrel conventions
  • references/patterns.md
    — Annotated code patterns for the API layer, query options, hooks, mutations, optimistic updates, contexts, and stores
  • references/directory-layout.md
    ——完整的目录结构、文件命名和桶约定
  • references/patterns.md
    ——API层、查询选项、钩子、突变、乐观更新、Context和存储的带注释代码模式