Loading...
Loading...
Compare original and translation side by side
scripts/
validate-state.sh
references/
state-patterns.mdscripts/
validate-state.sh
references/
state-patterns.mddiscoverdiscover:
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_onlydiscoverdiscover:
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_onlyprecision_readprecision_read:
files:
- path: "package.json"
extract: content
verbosity: minimal@tanstack/react-queryzustandreact-hook-formzodnuqsprecision_readprecision_read:
files:
- path: "package.json"
extract: content
verbosity: minimal@tanstack/react-queryzustandreact-hook-formzodnuqsprecision_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: standardprecision_read:
files:
- path: "src/lib/query-client.ts" # 或已发现的对应文件
extract: content
- path: "src/stores/user-store.ts" # 或已发现的对应文件
extract: content
verbosity: standardreferences/state-patterns.mdreferences/state-patterns.md| State Type | Best Tool | Use When |
|---|---|---|
| Server state | TanStack Query | Data from APIs, needs caching/invalidation |
| Client state | Zustand | UI state shared across components |
| Form state | React Hook Form + Zod | Complex forms with validation |
| URL state | nuqs or searchParams | Sharable, bookmarkable state |
| Component state | useState | Local to one component |
useState| 状态类型 | 最佳工具 | 使用场景 |
|---|---|---|
| 服务端状态 | TanStack Query | API数据,需要缓存/失效机制 |
| 客户端状态 | Zustand | 组件间共享的UI状态 |
| 表单状态 | React Hook Form + Zod | 带复杂验证的表单 |
| URL状态 | nuqs 或 searchParams | 可分享、可收藏的状态 |
| 组件内部状态 | useState | 仅单个组件使用的本地状态 |
useStatenpm install @tanstack/react-query # Note: Targeting TanStack Query v5
npm install -D @tanstack/react-query-devtoolsnpm install @tanstack/react-query # 注意:目标版本为TanStack Query v5
npm install -D @tanstack/react-query-devtools// 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,
},
},
});// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1分钟
retry: 1,
refetchOnWindowFocus: false,
},
},
});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} />;
}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, // 无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) => {
// 取消正在进行的相关查询
await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });
// 记录之前的数值
const previousUser = queryClient.getQueryData(['user', newUser.id]);
// 执行乐观更新
queryClient.setQueryData(['user', newUser.id], newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// 出错时回滚
queryClient.setQueryData(
['user', newUser.id],
context?.previousUser
);
},
onSettled: (data, error, variables) => {
// 无论成功或失败,重新获取数据
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
},
});
}// 使所有用户相关查询失效
queryClient.invalidateQueries({ queryKey: ['user'] });
// 使特定用户的查询失效
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// 从缓存中彻底移除
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} />;
}npm install zustandnpm install zustandimport { 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 }),
}));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);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 }),
}));import { create, StateCreator } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// 分片模式用于组织代码
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' }
)
)
);// 通过选择器仅获取需要的状态,避免不必要的重渲染
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);npm install react-hook-form zod @hookform/resolversnpm install react-hook-form zod @hookform/resolversimport { 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>;import { z } from 'zod';
export const userSchema = z.object({
email: z.string().email('无效的邮箱地址'),
name: z.string().min(2, '姓名至少需要2个字符'),
age: z.number().min(18, '必须年满18岁'),
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>;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>
);
}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 ? '提交中...' : '提交'}
</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, '至少需要添加一个用户'),
});
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)}>
删除
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: '', email: '' })}
>
添加用户
</button>
</div>
);
}npm install nuqs # Note: Targeting nuqs v1.ximport { 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
}npm install nuqs # 注意:目标版本为nuqs v1.ximport { 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
// 自动同步、类型安全、可收藏
}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()}`);
};
}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()}`);
};
}precision_writeprecision_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_onlyprecision_writeprecision_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_onlybash scripts/validate-state.sh .scripts/validate-state.shbash scripts/validate-state.sh .scripts/validate-state.shprecision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimalprecision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal// Server data with TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });
// UI state with Zustand
const { sidebarOpen, toggleSidebar } = useUIStore();// 使用TanStack Query获取服务端数据
const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });
// 使用Zustand管理UI状态
const { sidebarOpen, toggleSidebar } = useUIStore();const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const onSubmit = (data: UserFormData) => {
mutation.mutate(data);
};const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const onSubmit = (data: UserFormData) => {
mutation.mutate(data);
};const [page] = useQueryState('page', parseAsInteger.withDefault(1));
const [search] = useQueryState('search');
const { data } = useQuery({
queryKey: ['products', page, search],
queryFn: () => getProducts({ page, search }),
});const [page] = useQueryState('page', parseAsInteger.withDefault(1));
const [search] = useQueryState('search');
const { data } = useQuery({
queryKey: ['products', page, search],
queryFn: () => getProducts({ page, search }),
});anyanydiscover: { 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.mddiscover: { queries: [state_libraries, data_fetching, forms], verbosity: files_only }
precision_read: { files: ["package.json", 示例状态仓库文件], extract: content }precision_write: { files: [query-client, 状态仓库, 验证Schema], verbosity: count_only }precision_exec: { commands: [{ cmd: "npm run typecheck" }], verbosity: minimal }bash scripts/validate-state.sh .references/state-patterns.md