关于 state-management
面向 TypeScript 应用的状态管理规划与实现技能,帮助开发者为服务端、客户端、表单、组件及 URL 状态选择方案,并验证实现。
主要功能
- 01发现现有状态库
- 02按状态类型选择方案
- 03TanStack Query 模式
- 04Zustand Store 模式
- 05React Hook Form 与 Zod 表单
- 06URL 状态实现与验证
适用场景
- 01为现有代码库选择状态工具
- 02添加带缓存的 API 数据与变更
- 03构建复杂的校验表单
- 04迁移状态方案并检查实现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mgd34msu/ goodvibes-plugin --skill state-management安装后在 Claude Code 中输入/state-management确认已加载。
附带文件references/state-patterns.mdscripts/validate-state.sh
详细介绍
资源
scripts/
validate-state.sh
references/
state-patterns.md
状态管理
本技能将引导你使用 GoodVibes 精准工具进行状态架构决策和实现。在选择状态解决方案、实现数据获取模式或管理复杂表单状态时,请使用此工作流。
何时使用此技能
在以下情况下加载此技能:
- 决定采用哪种状态管理方式
- 实现带缓存的服务端数据获取
- 构建带复杂验证的表单
- 管理客户端应用状态
- 实现基于 URL 的状态模式
- 从一种状态解决方案迁移到另一种
触发短语:“状态管理”、“TanStack Query”、“Zustand”、“React Hook Form”、“表单验证”、“数据获取”、“缓存失效”、“URL 状态”。
核心工作流
阶段 1:探索
选择状态解决方案前,先了解现有模式。
步骤 1.1:识别当前状态库
使用 discover 查找现有的状态管理解决方案。
discover:
queries:
- id: state_libraries
type: grep
pattern: "(from 'zustand'|from '@tanstack/react-query'|from 'react-hook-form'|from 'jotai'|from 'redux'|useContext)"
glob: "**/*.{ts,tsx}"
- id: data_fetching
type: grep
pattern: "(useQuery|useMutation|useSWR|fetch|axios)"
glob: "**/*.{ts,tsx}"
- id: form_libraries
type: grep
pattern: "(useForm|Formik|react-hook-form)"
glob: "**/*.{ts,tsx}"
verbosity: files_only
可以了解:
- 正在使用的状态管理库
- 数据获取模式(REST、GraphQL 等)
- 表单处理方式
- 代码库的一致性
步骤 1.2:分析软件包依赖
使用 precision_read 检查已安装的内容。
precision_read:
files:
- path: "package.json"
extract: content
verbosity: minimal
查找:
@tanstack/react-query(服务端状态)zustand(客户端状态)react-hook-form+zod(表单)nuqs或类似工具(URL 状态)
步骤 1.3:阅读现有状态模式
阅读 2-3 个示例,了解实现模式。
precision_read:
files:
- path: "src/lib/query-client.ts" # or discovered file
extract: content
- path: "src/stores/user-store.ts" # or discovered file
extract: content
verbosity: standard
阶段 2:状态分类
为每种状态选择合适的工具。完整决策树请参阅 references/state-patterns.md。
快速决策指南
| 状态类型 | 最佳工具 | 适用场景 |
|---|---|---|
| 服务端状态 | TanStack Query | 来自 API、需要缓存/失效的数据 |
| 客户端状态 | Zustand | 在多个组件间共享的 UI 状态 |
| 表单状态 | React Hook Form + Zod | 带验证的复杂表单 |
| URL 状态 | nuqs 或 searchParams | 可分享、可加入书签的状态 |
| 组件状态 | useState | 仅属于单个组件的状态 |
状态就近放置原则:
尽可能让状态靠近其使用位置。先使用 useState,需要共享时提升到父组件,然后仅在必要时考虑专用解决方案。
阶段 3:使用 TanStack Query 管理服务端状态
适用于需要缓存、后台更新和乐观变更的 API 数据。
步骤 3.1:安装依赖
检查是否已安装,否则添加:
npm install @tanstack/react-query # Note: Targeting TanStack Query v5
npm install -D @tanstack/react-query-devtools
步骤 3.2:设置 Query 客户端
创建 Query 客户端配置。
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
retry: 1,
refetchOnWindowFocus: false,
},
},
});
步骤 3.3:实现 Query 模式
基本 Query:
import { useQuery } from '@tanstack/react-query';
import { getUser } from '@/lib/api';
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => getUser(userId),
enabled: !!userId, // Don't run if no userId
});
}
带乐观更新的变更:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateUser } from '@/lib/api';
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(['user', newUser.id]);
// Optimistically update
queryClient.setQueryData(['user', newUser.id], newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(
['user', newUser.id],
context?.previousUser
);
},
onSettled: (data, error, variables) => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
},
});
}
缓存失效:
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ['user'] });
// Invalidate specific user
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['user', userId] });
使用错误和加载状态:
function UserProfile({ userId }: { userId: string }) {
const { data, isPending, isError, error } = useUser(userId);
if (isPending) return <Skeleton />;
if (isError) return <ErrorDisplay error={error} />;
return <UserProfile user={data} />;
}
阶段 4:使用 Zustand 管理客户端状态
适用于在多个组件间共享的 UI 状态(模态框、主题、筛选器)。
步骤 4.1:安装依赖
npm install zustand
步骤 4.2:创建 Store
简单 Store:
import { create } from 'zustand';
interface UIStore {
sidebarOpen: boolean;
toggleSidebar: () => void;
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
export const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
带切片的 Store:
import { create, StateCreator } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// Slice pattern for organization
interface AuthSlice {
user: User | null;
setUser: (user: User | null) => void;
}
interface UISlice {
sidebarOpen: boolean;
toggleSidebar: () => void;
}
type Store = AuthSlice & UISlice;
const createAuthSlice: StateCreator<Store, [], [], AuthSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
const createUISlice: StateCreator<Store, [], [], UISlice> = (set) => ({
sidebarOpen: true,
toggleSidebar: () => set((state: Store) => ({
sidebarOpen: !state.sidebarOpen
})),
});
export const useStore = create<Store>()(
devtools(
persist(
(...a) => ({
...createAuthSlice(...a),
...createUISlice(...a),
}),
{ name: 'app-store' }
)
)
);
使用选择器:
// Avoid re-renders by selecting only what you need
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
阶段 5:使用 React Hook Form + Zod 管理表单状态
适用于带验证、字段数组和嵌套对象的复杂表单。
步骤 5.1:安装依赖
npm install react-hook-form zod @hookform/resolvers
步骤 5.2:定义验证架构
import { z } from 'zod';
export const userSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(2, 'Name must be at least 2 characters'),
age: z.number().min(18, 'Must be 18 or older'),
role: z.enum(['user', 'admin']).default('user'),
preferences: z.object({
newsletter: z.boolean().default(false),
notifications: z.boolean().default(true),
}),
});
export type UserFormData = z.infer<typeof userSchema>;
步骤 5.3:实现表单
基本表单:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { userSchema, type UserFormData } from './schema';
export function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: {
role: 'user',
preferences: {
newsletter: false,
notifications: true,
},
},
});
const onSubmit = async (data: UserFormData) => {
await createUser(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
字段数组:
import { useFieldArray } from 'react-hook-form';
const schema = z.object({
users: z.array(
z.object({
name: z.string().min(1),
email: z.string().email(),
})
).min(1, 'At least one user required'),
});
function UsersForm() {
const { control, register } = useForm({
resolver: zodResolver(schema),
});
const { fields, append, remove } = useFieldArray({
control,
name: 'users',
});
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`users.${index}.name`)} />
<input {...register(`users.${index}.email`)} />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: '', email: '' })}
>
Add User
</button>
</div>
);
}
阶段 6:URL 状态模式
适用于应当可分享并可加入书签的状态。
步骤 6.1:使用 nuqs(推荐)
npm install nuqs # Note: Targeting nuqs v1.x
import { useQueryState, parseAsInteger, parseAsStringEnum } from 'nuqs';
export function ProductList() {
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1)
);
const [sort, setSort] = useQueryState(
'sort',
parseAsStringEnum(['name', 'price', 'date']).withDefault('name')
);
// URL: /products?page=2&sort=price
// Automatically synced, type-safe, bookmarkable
}
步骤 6.2:使用 Next.js searchParams
import { useSearchParams, useRouter } from 'next/navigation';
export function ProductList() {
const searchParams = useSearchParams();
const router = useRouter();
const page = Number(searchParams.get('page')) || 1;
const setPage = (newPage: number) => {
const params = new URLSearchParams(searchParams);
params.set('page', String(newPage));
router.push(`?${params.toString()}`);
};
}
阶段 7:使用精准工具实现
使用 precision_write 创建状态管理文件。
precision_write:
files:
- path: "src/lib/query-client.ts"
content: |
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({ ... });
- path: "src/stores/ui-store.ts"
content: |
import { create } from 'zustand';
export const useUIStore = create({ ... });
- path: "src/schemas/user-schema.ts"
content: |
import { z } from 'zod';
export const userSchema = z.object({ ... });
verbosity: count_only
阶段 8:验证
步骤 8.1:运行验证脚本
使用验证脚本确保质量。
bash scripts/validate-state.sh .
完整验证套件请参阅 scripts/validate-state.sh。
步骤 8.2:类型检查
验证 TypeScript 编译。
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
常见模式
模式 1:结合 TanStack Query + Zustand
// Server data with TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });
// UI state with Zustand
const { sidebarOpen, toggleSidebar } = useUIStore();
模式 2:通过服务端变更处理表单
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const onSubmit = (data: UserFormData) => {
mutation.mutate(data);
};
模式 3:由 URL 状态驱动数据获取
const [page] = useQueryState('page', parseAsInteger.withDefault(1));
const [search] = useQueryState('search');
const { data } = useQuery({
queryKey: ['products', page, search],
queryFn: () => getProducts({ page, search }),
});
常见反模式
不要:
- 使用全局状态存储服务端数据(使用 TanStack Query)
- 把所有内容都放进 Zustand(尽可能就近存放)
- 手动管理表单状态(使用 React Hook Form)
- 在 URL 参数中存储 UI 状态(使用 Zustand)
- 对频繁变化的数据使用 Context(会导致重新渲染)
- 在变更后忘记使缓存失效
- 跳过表单验证方案
- 在状态存储中使用
any类型
应该:
- 根据状态类型选择合适的工具
- 尽可能就近存放状态
- 使用选择器避免重新渲染
- 实施乐观更新以改善 UX
- 使用 Zod 验证所有表单输入
- 使用 TypeScript 定义所有状态
- 在变更后使查询失效
- 保持查询键一致
快速参考
发现阶段:
discover: { queries: [state_libraries, data_fetching, forms], verbosity: files_only }
precision_read: { files: ["package.json", example stores], extract: content }
实施阶段:
precision_write: { files: [query-client, stores, schemas], verbosity: count_only }
验证阶段:
precision_exec: { commands: [{ cmd: "npm run typecheck" }], verbosity: minimal }
实施后:
bash scripts/validate-state.sh .
有关详细模式和决策树,请参阅 references/state-patterns.md。