app-renderer-systems
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFeature 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 directory.
systems/<domain>/Read for the full directory structure and naming conventions.
Read for annotated implementation patterns per layer.
references/directory-layout.mdreferences/patterns.md“系统”是一个独立的、领域驱动的模块,拥有与单个领域相关的所有内容:其API调用、查询层、hooks、组件和公共API。系统存放在目录下。
systems/<domain>/完整目录结构和命名规范请阅读。
各层带注释的实现模式请阅读。
references/directory-layout.mdreferences/patterns.mdQuick Reference
快速参考
Mandatory Companion Skills
必备配套技能
Activate alongside this skill — systems span multiple technical domains:
| Situation | Activate |
|---|---|
| Any hook or component | |
| Data fetching/caching | |
| Mutations | |
| XState store | |
| Utility functions | |
| Writing/fixing tests | |
| Bug fix | |
使用本指南时请同步激活以下技能——系统横跨多个技术领域:
| 场景 | 需激活的技能 |
|---|---|
| 任意hook或组件开发 | |
| 数据获取/缓存 | |
| 变更操作 | |
| XState存储 | |
| 工具函数开发 | |
| 编写/修复测试 | |
| Bug修复 | |
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 checkssystems/<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 hooks(查询、变更、视图模型)
│ ├── __tests__/
│ ├── use-<action>.ts # 查询hooks
│ ├── use-create-<entity>.ts # 变更hooks
│ ├── 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 on every function to support query cancellation.
signal?: AbortSignal - 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 on every key tuple.
as const
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 and
queryKeyviaqueryFnfor type safety and reuse.queryOptions - Export each option factory for use in hooks, prefetching, and route loaders.
- Always pass from the query context through to the API layer.
signal
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 - 导出每个选项工厂函数,供hooks、预取和路由加载器使用。
- 始终将查询上下文的传递到API层。
signal
Step 5 — Write hooks
步骤5 — 编写hooks
- Query hooks: Wrap with the
useQueryfactories; accept a scope ID + optionalqueryOptions.{ enabled? } - Mutation hooks: Use with proper
useMutation/onMutate/onErrorcallbacks for optimistic updates.onSettled - View-model hooks: Compose multiple hooks for a page/shell component; return a flat object.
- Place tests in or co-locate as
hooks/__tests__/.use-xxx.test.tsx
Read for complete mutation and optimistic update patterns.
references/patterns.md- 查询hooks:用工厂函数封装
queryOptions;接受作用域ID + 可选的useQuery参数。{ enabled? } - 变更hooks:使用并配置合适的
useMutation/onMutate/onError回调实现乐观更新。onSettled - 视图模型hooks:为页面/外壳组件组合多个hooks;返回扁平化对象。
- 测试代码放在目录下,或者就近存为
hooks/__tests__/。use-xxx.test.tsx
完整的变更和乐观更新模式请阅读。
references/patterns.mdStep 6 — (Optional) Add context
步骤6 —(可选)添加context
Create when query data or combined state must be shared across a component subtree without prop-drilling.
contexts/<domain>-context.tsxts
// 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.tsxts
// 始终使用可空context — 若在提供者外使用消费者hook会抛出错误
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);- 在同一个文件中导出context、provider组件,并重新导出消费者hooks。
- 对性能敏感的子树,可以拆分为核心/UI/操作子context。
Step 7 — (Optional) Add an XState store
步骤7 —(可选)添加XState存储
Create for complex async state machines (multi-step flows, polling, event emission).
stores/<domain>-store.tsts
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.tsts
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";
// Hooks
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
核心规则
- Use for co-location. Co-locate
queryOptionsandqueryKeyin reusable option factories. Never scatter the same query key across multiple files.queryFn - Unidirectional dependency flow. . Adapters never import from hooks or components.
adapters -> lib -> hooks -> components - 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.
- 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.
- AbortSignal propagation. Pass from the
signalcontext through to every API call for proper query cancellation.queryFn - Always invalidate after mutations. Use in
queryClient.invalidateQueriesto ensure eventual consistency with the server.onSettled - Optimistic updates require rollback. When using cache-based optimistic updates, snapshot previous data in and restore in
onMutate.onError - Cancel outgoing queries before optimistic updates. Call in
queryClient.cancelQueriesto prevent refetches from overwriting optimistic state.onMutate - Zod schemas in lib/. Place all Zod schemas in for runtime validation at API boundaries.
lib/<domain>-schemas.ts
- 使用实现同位置管理:将
queryOptions和queryKey放在可复用的选项工厂中,永远不要将同一个查询键分散在多个文件中。queryFn - 单向依赖流:。适配器永远不要从hooks或组件导入内容。
适配器 -> 工具库 -> hooks -> 组件 - 查询键设置作用域:任何依赖认证作用域(用户、组织、租户)的查询,都必须在键中包含对应作用域ID,避免跨作用域数据过期。
- API层使用带类型的错误:永远不要从适配器抛出原始错误,使用带类型的错误类,这样消费者无需检查消息字符串就能区分错误类型。
- AbortSignal传递:将上下文中的
queryFn传递到每个API调用,实现正确的查询取消。signal - 变更操作后始终执行失效:在中调用
onSettled,保证最终与服务端数据一致。queryClient.invalidateQueries - 乐观更新需要支持回滚:使用基于缓存的乐观更新时,在中快照之前的数据,并在
onMutate中恢复。onError - 乐观更新前取消正在进行的查询:在中调用
onMutate,避免重新拉取的数据覆盖乐观状态。queryClient.cancelQueries - Zod schema存放在lib/目录下:将所有Zod schema放在中,用于API边界的运行时校验。
lib/<domain>-schemas.ts
Error Handling
错误处理
- API layer throws typed error: TanStack Query catches and exposes it via .
query.error - Mutation fails with optimistic update: callback rolls back the cache to the snapshot from
onError, thenonMutateinvalidates to refetch fresh data.onSettled - Stale cross-scope data: Add the scope ID to the query key and verify that guards check
enabled.Boolean(scopeId) - Query cancellation on unmount: TanStack Query automatically cancels in-flight queries via the when a component unmounts — ensure
signalis propagated to the API layer.signal
- API层抛出带类型的错误:TanStack Query会捕获错误并通过暴露。
query.error - 乐观更新的变更操作失败:回调将缓存回滚到
onError中保存的快照,然后onMutate执行失效以拉取最新数据。onSettled - 跨作用域数据过期:在查询键中添加作用域ID,并确认守卫检查了
enabled。Boolean(scopeId) - 组件卸载时的查询取消:TanStack Query会在组件卸载时通过自动取消进行中的查询——请确保
signal已经传递到API层。signal
Detailed References
详细参考
- — Full directory structure, file naming, and barrel conventions
references/directory-layout.md - — Annotated code patterns for the API layer, query options, hooks, mutations, optimistic updates, contexts, and stores
references/patterns.md
- — 完整目录结构、文件命名和桶导出规范
references/directory-layout.md - — API层、查询选项、hooks、变更、乐观更新、context和存储的带注释代码模式
references/patterns.md