data-fetching
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLobeHub Data Fetching Architecture
LobeHub 数据获取架构
Related Skills:
- How to structure List and Detail data in stores (Map vs Array patterns)store-data-structures
相关技能:
- 如何在store中构建列表和详情数据(Map与Array模式)store-data-structures
Architecture Overview
架构概述
┌─────────────┐
│ Component │
└──────┬──────┘
│ 1. Call useFetchXxx hook from store
↓
┌──────────────────┐
│ Zustand Store │
│ (State + Hook) │
└──────┬───────────┘
│ 2. useClientDataSWR calls service
↓
┌──────────────────┐
│ Service Layer │
│ (xxxService) │
└──────┬───────────┘
│ 3. Call lambdaClient
↓
┌──────────────────┐
│ lambdaClient │
│ (TRPC Client) │
└──────────────────┘┌─────────────┐
│ Component │
└──────┬──────┘
│ 1. 从store中调用useFetchXxx钩子
↓
┌──────────────────┐
│ Zustand Store │
│ (状态 + 钩子) │
└──────┬───────────┘
│ 2. useClientDataSWR调用服务
↓
┌──────────────────┐
│ Service层 │
│ (xxxService) │
└──────┬───────────┘
│ 3. 调用lambdaClient
↓
┌──────────────────┐
│ lambdaClient │
│ (TRPC Client) │
└──────────────────┘Core Principles
核心原则
✅ DO
✅ 推荐做法
- Use Service Layer for all API calls
- Use Store SWR Hooks for data fetching (not useEffect)
- Use proper data structures - See skill for List vs Detail patterns
store-data-structures - Use lambdaClient.mutate for write operations (create/update/delete)
- Use lambdaClient.query only inside service methods
- 使用Service层处理所有API调用
- 使用Store的SWR钩子进行数据获取(而非useEffect)
- 使用合适的数据结构 - 参考技能中的列表与详情模式
store-data-structures - 使用lambdaClient.mutate处理写入操作(创建/更新/删除)
- 仅在服务方法内使用lambdaClient.query
❌ DON'T
❌ 禁止做法
- Never use useEffect for data fetching
- Never call lambdaClient directly in components or stores
- Never use useState for server data
- Never mix data structure patterns - Follow skill
store-data-structures
Note: For data structure patterns (Map vs Array, List vs Detail), see theskill.store-data-structures
- 绝不要使用useEffect进行数据获取
- 绝不要在组件或store中直接调用lambdaClient
- 绝不要使用useState存储服务端数据
- 绝不要混合使用数据结构模式 - 遵循技能的规范
store-data-structures
注意: 关于数据结构模式(Map vs Array、列表vs详情),请参考技能。store-data-structures
Layer 1: Service Layer
第一层:Service层
Purpose
用途
- Encapsulate all API calls to lambdaClient
- Provide clean, typed interfaces
- Single source of truth for API operations
- 封装所有对lambdaClient的API调用
- 提供清晰的类型化接口
- 作为API操作的唯一可信源
Service Structure
服务结构
typescript
// src/services/agentEval.ts
import { lambdaClient } from '@/libs/trpc/client';
class AgentEvalService {
// Query methods - READ operations
async listBenchmarks() {
return lambdaClient.agentEval.listBenchmarks.query();
}
async getBenchmark(id: string) {
return lambdaClient.agentEval.getBenchmark.query({ id });
}
// Mutation methods - WRITE operations
async createBenchmark(params: CreateBenchmarkParams) {
return lambdaClient.agentEval.createBenchmark.mutate(params);
}
async updateBenchmark(params: UpdateBenchmarkParams) {
return lambdaClient.agentEval.updateBenchmark.mutate(params);
}
async deleteBenchmark(id: string) {
return lambdaClient.agentEval.deleteBenchmark.mutate({ id });
}
}
export const agentEvalService = new AgentEvalService();typescript
// src/services/agentEval.ts
import { lambdaClient } from '@/libs/trpc/client';
class AgentEvalService {
// 查询方法 - 读取操作
async listBenchmarks() {
return lambdaClient.agentEval.listBenchmarks.query();
}
async getBenchmark(id: string) {
return lambdaClient.agentEval.getBenchmark.query({ id });
}
// 变更方法 - 写入操作
async createBenchmark(params: CreateBenchmarkParams) {
return lambdaClient.agentEval.createBenchmark.mutate(params);
}
async updateBenchmark(params: UpdateBenchmarkParams) {
return lambdaClient.agentEval.updateBenchmark.mutate(params);
}
async deleteBenchmark(id: string) {
return lambdaClient.agentEval.deleteBenchmark.mutate({ id });
}
}
export const agentEvalService = new AgentEvalService();Service Guidelines
服务规范
- One service per domain (e.g., agentEval, ragEval, aiAgent)
- Export singleton instance ()
export const xxxService = new XxxService() - Method names match operations (list, get, create, update, delete)
- Clear parameter types (use interfaces for complex params)
- 每个领域对应一个服务(例如agentEval、ragEval、aiAgent)
- 导出单例实例()
export const xxxService = new XxxService() - 方法名与操作匹配(list、get、create、update、delete)
- 清晰的参数类型(复杂参数使用接口定义)
Layer 2: Store with SWR Hooks
第二层:带SWR钩子的Store
Purpose
用途
- Manage client-side state
- Provide SWR hooks for data fetching
- Handle cache invalidation
Data Structure: Seeskill for how to structure List and Detail data.store-data-structures
- 管理客户端状态
- 提供用于数据获取的SWR钩子
- 处理缓存失效
数据结构: 参考技能了解如何构建列表和详情数据。store-data-structures
Store Structure Overview
Store结构概述
typescript
// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
// List data - simple array (see store-data-structures skill)
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// Detail data - map for caching (see store-data-structures skill)
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
// Mutation states
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}For complete initialState, reducer, and internal dispatch patterns, see theskill.store-data-structures
typescript
// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
// 列表数据 - 简单数组(参考store-data-structures技能)
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// 详情数据 - 用于缓存的Map(参考store-data-structures技能)
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
// 变更状态
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}完整的initialState、reducer和内部dispatch模式,请参考技能。store-data-structures
Create Actions
创建Actions
typescript
// src/store/eval/slices/benchmark/action.ts
import type { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';
import isEqual from 'fast-deep-equal';
import { mutate, useClientDataSWR } from '@/libs/swr';
import { agentEvalService } from '@/services/agentEval';
import type { EvalStore } from '@/store/eval/store';
import { benchmarkDetailReducer, type BenchmarkDetailDispatch } from './reducer';
const FETCH_BENCHMARKS_KEY = 'FETCH_BENCHMARKS';
const FETCH_BENCHMARK_DETAIL_KEY = 'FETCH_BENCHMARK_DETAIL';
export interface BenchmarkAction {
// SWR Hooks - for data fetching
useFetchBenchmarks: () => SWRResponse;
useFetchBenchmarkDetail: (id?: string) => SWRResponse;
// Refresh methods - for cache invalidation
refreshBenchmarks: () => Promise<void>;
refreshBenchmarkDetail: (id: string) => Promise<void>;
// Mutation actions - for write operations
createBenchmark: (params: CreateParams) => Promise<any>;
updateBenchmark: (params: UpdateParams) => Promise<void>;
deleteBenchmark: (id: string) => Promise<void>;
// Internal methods - not for direct UI use
internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
}
export const createBenchmarkSlice: StateCreator<
EvalStore,
[['zustand/devtools', never]],
[],
BenchmarkAction
> = (set, get) => ({
// Fetch list - Simple array
useFetchBenchmarks: () => {
return useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {
onSuccess: (data: any) => {
set(
{
benchmarkList: data,
benchmarkListInit: true,
},
false,
'useFetchBenchmarks/success',
);
},
});
},
// Fetch detail - Map with dispatch
useFetchBenchmarkDetail: (id) => {
return useClientDataSWR(
id ? [FETCH_BENCHMARK_DETAIL_KEY, id] : null,
() => agentEvalService.getBenchmark(id!),
{
onSuccess: (data: any) => {
get().internal_dispatchBenchmarkDetail({
type: 'setBenchmarkDetail',
id: id!,
value: data,
});
get().internal_updateBenchmarkDetailLoading(id!, false);
},
},
);
},
// Refresh methods
refreshBenchmarks: async () => {
await mutate(FETCH_BENCHMARKS_KEY);
},
refreshBenchmarkDetail: async (id) => {
await mutate([FETCH_BENCHMARK_DETAIL_KEY, id]);
},
// CREATE - Refresh list after creation
createBenchmark: async (params) => {
set({ isCreatingBenchmark: true }, false, 'createBenchmark/start');
try {
const result = await agentEvalService.createBenchmark(params);
await get().refreshBenchmarks();
return result;
} finally {
set({ isCreatingBenchmark: false }, false, 'createBenchmark/end');
}
},
// UPDATE - With optimistic update for detail
updateBenchmark: async (params) => {
const { id } = params;
// 1. Optimistic update
get().internal_dispatchBenchmarkDetail({
type: 'updateBenchmarkDetail',
id,
value: params,
});
// 2. Set loading
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. Call service
await agentEvalService.updateBenchmark(params);
// 4. Refresh from server
await get().refreshBenchmarks();
await get().refreshBenchmarkDetail(id);
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// DELETE - Refresh list and remove from detail map
deleteBenchmark: async (id) => {
// 1. Optimistic update
get().internal_dispatchBenchmarkDetail({
type: 'deleteBenchmarkDetail',
id,
});
// 2. Set loading
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. Call service
await agentEvalService.deleteBenchmark(id);
// 4. Refresh list
await get().refreshBenchmarks();
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// Internal - Dispatch to reducer (for detail map)
internal_dispatchBenchmarkDetail: (payload) => {
const currentMap = get().benchmarkDetailMap;
const nextMap = benchmarkDetailReducer(currentMap, payload);
// No need to update if map is the same
if (isEqual(nextMap, currentMap)) return;
set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`);
},
// Internal - Update loading state for specific detail
internal_updateBenchmarkDetailLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };
}
return {
loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id),
};
},
false,
'updateBenchmarkDetailLoading',
);
},
});typescript
// src/store/eval/slices/benchmark/action.ts
import type { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';
import isEqual from 'fast-deep-equal';
import { mutate, useClientDataSWR } from '@/libs/swr';
import { agentEvalService } from '@/services/agentEval';
import type { EvalStore } from '@/store/eval/store';
import { benchmarkDetailReducer, type BenchmarkDetailDispatch } from './reducer';
const FETCH_BENCHMARKS_KEY = 'FETCH_BENCHMARKS';
const FETCH_BENCHMARK_DETAIL_KEY = 'FETCH_BENCHMARK_DETAIL';
export interface BenchmarkAction {
// SWR钩子 - 用于数据获取
useFetchBenchmarks: () => SWRResponse;
useFetchBenchmarkDetail: (id?: string) => SWRResponse;
// 刷新方法 - 用于缓存失效
refreshBenchmarks: () => Promise<void>;
refreshBenchmarkDetail: (id: string) => Promise<void>;
// 变更操作 - 用于写入操作
createBenchmark: (params: CreateParams) => Promise<any>;
updateBenchmark: (params: UpdateParams) => Promise<void>;
deleteBenchmark: (id: string) => Promise<void>;
// 内部方法 - 不直接用于UI
internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
}
export const createBenchmarkSlice: StateCreator<
EvalStore,
[['zustand/devtools', never]],
[],
BenchmarkAction
> = (set, get) => ({
// 获取列表 - 简单数组
useFetchBenchmarks: () => {
return useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {
onSuccess: (data: any) => {
set(
{
benchmarkList: data,
benchmarkListInit: true,
},
false,
'useFetchBenchmarks/success',
);
},
});
},
// 获取详情 - 带dispatch的Map
useFetchBenchmarkDetail: (id) => {
return useClientDataSWR(
id ? [FETCH_BENCHMARK_DETAIL_KEY, id] : null,
() => agentEvalService.getBenchmark(id!),
{
onSuccess: (data: any) => {
get().internal_dispatchBenchmarkDetail({
type: 'setBenchmarkDetail',
id: id!,
value: data,
});
get().internal_updateBenchmarkDetailLoading(id!, false);
},
},
);
},
// 刷新方法
refreshBenchmarks: async () => {
await mutate(FETCH_BENCHMARKS_KEY);
},
refreshBenchmarkDetail: async (id) => {
await mutate([FETCH_BENCHMARK_DETAIL_KEY, id]);
},
// 创建 - 创建后刷新列表
createBenchmark: async (params) => {
set({ isCreatingBenchmark: true }, false, 'createBenchmark/start');
try {
const result = await agentEvalService.createBenchmark(params);
await get().refreshBenchmarks();
return result;
} finally {
set({ isCreatingBenchmark: false }, false, 'createBenchmark/end');
}
},
// 更新 - 对详情进行乐观更新
updateBenchmark: async (params) => {
const { id } = params;
// 1. 乐观更新
get().internal_dispatchBenchmarkDetail({
type: 'updateBenchmarkDetail',
id,
value: params,
});
// 2. 设置加载状态
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. 调用服务
await agentEvalService.updateBenchmark(params);
// 4. 从服务端刷新
await get().refreshBenchmarks();
await get().refreshBenchmarkDetail(id);
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// 删除 - 刷新列表并从详情Map中移除
deleteBenchmark: async (id) => {
// 1. 乐观更新
get().internal_dispatchBenchmarkDetail({
type: 'deleteBenchmarkDetail',
id,
});
// 2. 设置加载状态
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. 调用服务
await agentEvalService.deleteBenchmark(id);
// 4. 刷新列表
await get().refreshBenchmarks();
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// 内部方法 - 向reducer分发事件(针对详情Map)
internal_dispatchBenchmarkDetail: (payload) => {
const currentMap = get().benchmarkDetailMap;
const nextMap = benchmarkDetailReducer(currentMap, payload);
// 如果Map未变化则无需更新
if (isEqual(nextMap, currentMap)) return;
set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`);
},
// 内部方法 - 更新特定详情的加载状态
internal_updateBenchmarkDetailLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };
}
return {
loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id),
};
},
false,
'updateBenchmarkDetailLoading',
);
},
});Store Guidelines
Store规范
- SWR keys as constants at top of file
- useClientDataSWR for all data fetching (never useEffect)
- onSuccess callback updates store state
- Refresh methods use to invalidate cache
mutate() - Loading states in initialState, updated in onSuccess
- Mutations call service, then refresh relevant cache
- SWR键作为常量定义在文件顶部
- 使用useClientDataSWR进行所有数据获取(绝不使用useEffect)
- 在onSuccess回调中更新store状态
- **刷新方法使用**使缓存失效
mutate() - 加载状态定义在initialState中,在onSuccess中更新
- 变更操作调用服务,然后刷新相关缓存
Layer 3: Component Usage
第三层:组件使用
Data Fetching in Components
组件中的数据获取
Fetching List Data:
typescript
// Component using list data - ✅ CORRECT
import { useEvalStore } from '@/store/eval';
const BenchmarkList = () => {
// 1. Get the hook from store
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
// 2. Get list data
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
// 3. Call the hook (SWR handles the data fetching)
useFetchBenchmarks();
// 4. Use the data
if (!isInit) return <Loading />;
return (
<div>
<h2>Total: {benchmarks.length}</h2>
{benchmarks.map(b => <BenchmarkCard key={b.id} {...b} />)}
</div>
);
};Fetching Detail Data:
typescript
// Component using detail data from map - ✅ CORRECT
import { useEvalStore } from '@/store/eval';
import { useParams } from 'react-router-dom';
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
// 1. Get the hook
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
// 2. Get detail from map
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
// 3. Get loading state
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
// 4. Call the hook
useFetchBenchmarkDetail(benchmarkId);
// 5. Use the data
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
<p>{benchmark.description}</p>
{isLoading && <Spinner />}
</div>
);
};Using Selectors (Recommended):
typescript
// src/store/eval/slices/benchmark/selectors.ts
export const benchmarkSelectors = {
getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id],
isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) =>
s.loadingBenchmarkDetailIds.includes(id),
};
// Component with selectors
const BenchmarkDetail = () => {
const { benchmarkId } = useParams();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!));
useFetchBenchmarkDetail(benchmarkId);
return <div>{benchmark && <h1>{benchmark.name}</h1>}</div>;
};获取列表数据:
typescript
// 使用列表数据的组件 - ✅ 正确做法
import { useEvalStore } from '@/store/eval';
const BenchmarkList = () => {
// 1. 从store中获取钩子
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
// 2. 获取列表数据
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
// 3. 调用钩子(SWR处理数据获取)
useFetchBenchmarks();
// 4. 使用数据
if (!isInit) return <Loading />;
return (
<div>
<h2>总数: {benchmarks.length}</h2>
{benchmarks.map(b => <BenchmarkCard key={b.id} {...b} />)}
</div>
);
};获取详情数据:
typescript
// 使用Map中详情数据的组件 - ✅ 正确做法
import { useEvalStore } from '@/store/eval';
import { useParams } from 'react-router-dom';
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
// 1. 获取钩子
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
// 2. 从Map中获取详情
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
// 3. 获取加载状态
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
// 4. 调用钩子
useFetchBenchmarkDetail(benchmarkId);
// 5. 使用数据
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
<p>{benchmark.description}</p>
{isLoading && <Spinner />}
</div>
);
};使用选择器(推荐):
typescript
// src/store/eval/slices/benchmark/selectors.ts
export const benchmarkSelectors = {
getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id],
isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) =>
s.loadingBenchmarkDetailIds.includes(id),
};
// 使用选择器的组件
const BenchmarkDetail = () => {
const { benchmarkId } = useParams();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!));
useFetchBenchmarkDetail(benchmarkId);
return <div>{benchmark && <h1>{benchmark.name}</h1>}</div>;
};What NOT to Do
禁止做法
typescript
// ❌ WRONG - Don't use useEffect for data fetching
const BenchmarkList = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
const result = await lambdaClient.agentEval.listBenchmarks.query();
setData(result);
setLoading(false);
};
fetchData();
}, []);
return <div>...</div>;
};typescript
// ❌ 错误做法 - 不要使用useEffect进行数据获取
const BenchmarkList = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
const result = await lambdaClient.agentEval.listBenchmarks.query();
setData(result);
setLoading(false);
};
fetchData();
}, []);
return <div>...</div>;
};Mutations in Components
组件中的变更操作
typescript
// Mutations (Create/Update/Delete) with optimistic updates - ✅ CORRECT
import { useEvalStore } from '@/store/eval';
import { benchmarkSelectors } from '@/store/eval/selectors';
const CreateBenchmarkModal = () => {
const createBenchmark = useEvalStore((s) => s.createBenchmark);
const handleSubmit = async (values) => {
try {
// Optimistic update happens inside createBenchmark
await createBenchmark(values);
message.success('Created successfully');
onClose();
} catch (error) {
message.error('Failed to create');
}
};
return <Form onSubmit={handleSubmit}>...</Form>;
};
// With loading state for specific item
const BenchmarkItem = ({ id }: { id: string }) => {
const updateBenchmark = useEvalStore((s) => s.updateBenchmark);
const deleteBenchmark = useEvalStore((s) => s.deleteBenchmark);
const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmark(id));
const handleUpdate = async (data) => {
await updateBenchmark({ id, ...data });
};
const handleDelete = async () => {
await deleteBenchmark(id);
};
return (
<div>
{isLoading && <Spinner />}
<button onClick={handleUpdate}>Update</button>
<button onClick={handleDelete}>Delete</button>
</div>
);
};Data Structures: For detailed comparison of List vs Detail patterns, see theskill.store-data-structures
typescript
// 带乐观更新的变更操作(创建/更新/删除)- ✅ 正确做法
import { useEvalStore } from '@/store/eval';
import { benchmarkSelectors } from '@/store/eval/selectors';
const CreateBenchmarkModal = () => {
const createBenchmark = useEvalStore((s) => s.createBenchmark);
const handleSubmit = async (values) => {
try {
// 乐观更新在createBenchmark内部处理
await createBenchmark(values);
message.success('创建成功');
onClose();
} catch (error) {
message.error('创建失败');
}
};
return <Form onSubmit={handleSubmit}>...</Form>;
};
// 带特定项加载状态的组件
const BenchmarkItem = ({ id }: { id: string }) => {
const updateBenchmark = useEvalStore((s) => s.updateBenchmark);
const deleteBenchmark = useEvalStore((s) => s.deleteBenchmark);
const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmark(id));
const handleUpdate = async (data) => {
await updateBenchmark({ id, ...data });
};
const handleDelete = async () => {
await deleteBenchmark(id);
};
return (
<div>
{isLoading && <Spinner />}
<button onClick={handleUpdate}>更新</button>
<button onClick={handleDelete}>删除</button>
</div>
);
};数据结构: 关于列表与详情模式的详细对比,请参考技能。store-data-structures
Complete Example: Adding a New Feature
完整示例:添加新功能
Scenario: Add "Dataset" data fetching with optimistic updates
场景:添加带乐观更新的"数据集"数据获取
Step 1: Create Service
步骤1:创建服务
typescript
// src/services/agentEval.ts
class AgentEvalService {
// ... existing methods ...
// Add new methods
async listDatasets(benchmarkId: string) {
return lambdaClient.agentEval.listDatasets.query({ benchmarkId });
}
async getDataset(id: string) {
return lambdaClient.agentEval.getDataset.query({ id });
}
async createDataset(params: CreateDatasetParams) {
return lambdaClient.agentEval.createDataset.mutate(params);
}
}typescript
// src/services/agentEval.ts
class AgentEvalService {
// ... 现有方法 ...
// 添加新方法
async listDatasets(benchmarkId: string) {
return lambdaClient.agentEval.listDatasets.query({ benchmarkId });
}
async getDataset(id: string) {
return lambdaClient.agentEval.getDataset.query({ id });
}
async createDataset(params: CreateDatasetParams) {
return lambdaClient.agentEval.createDataset.mutate(params);
}
}Step 2: Create Reducer
步骤2:创建Reducer
typescript
// src/store/eval/slices/dataset/reducer.ts
import { produce } from 'immer';
import type { Dataset } from '@/types/dataset';
type AddDatasetAction = {
type: 'addDataset';
value: Dataset;
};
type UpdateDatasetAction = {
id: string;
type: 'updateDataset';
value: Partial<Dataset>;
};
type DeleteDatasetAction = {
id: string;
type: 'deleteDataset';
};
export type DatasetDispatch = AddDatasetAction | UpdateDatasetAction | DeleteDatasetAction;
export const datasetReducer = (state: Dataset[] = [], payload: DatasetDispatch): Dataset[] => {
switch (payload.type) {
case 'addDataset': {
return produce(state, (draft) => {
draft.unshift(payload.value);
});
}
case 'updateDataset': {
return produce(state, (draft) => {
const index = draft.findIndex((item) => item.id === payload.id);
if (index !== -1) {
draft[index] = { ...draft[index], ...payload.value };
}
});
}
case 'deleteDataset': {
return produce(state, (draft) => {
const index = draft.findIndex((item) => item.id === payload.id);
if (index !== -1) {
draft.splice(index, 1);
}
});
}
default:
return state;
}
};typescript
// src/store/eval/slices/dataset/reducer.ts
import { produce } from 'immer';
import type { Dataset } from '@/types/dataset';
type AddDatasetAction = {
type: 'addDataset';
value: Dataset;
};
type UpdateDatasetAction = {
id: string;
type: 'updateDataset';
value: Partial<Dataset>;
};
type DeleteDatasetAction = {
id: string;
type: 'deleteDataset';
};
export type DatasetDispatch = AddDatasetAction | UpdateDatasetAction | DeleteDatasetAction;
export const datasetReducer = (state: Dataset[] = [], payload: DatasetDispatch): Dataset[] => {
switch (payload.type) {
case 'addDataset': {
return produce(state, (draft) => {
draft.unshift(payload.value);
});
}
case 'updateDataset': {
return produce(state, (draft) => {
const index = draft.findIndex((item) => item.id === payload.id);
if (index !== -1) {
draft[index] = { ...draft[index], ...payload.value };
}
});
}
case 'deleteDataset': {
return produce(state, (draft) => {
const index = draft.findIndex((item) => item.id === payload.id);
if (index !== -1) {
draft.splice(index, 1);
}
});
}
default:
return state;
}
};Step 3: Create Store Slice
步骤3:创建Store切片
typescript
// src/store/eval/slices/dataset/initialState.ts
import type { Dataset } from '@/types/dataset';
export interface DatasetData {
currentPage: number;
hasMore: boolean;
isLoading: boolean;
items: Dataset[];
pageSize: number;
total: number;
}
export interface DatasetSliceState {
// Map keyed by benchmarkId
datasetMap: Record<string, DatasetData>;
// Simple state for single item (read-only, used in modals)
datasetDetail: Dataset | null;
isLoadingDatasetDetail: boolean;
loadingDatasetIds: string[];
}
export const datasetInitialState: DatasetSliceState = {
datasetMap: {},
datasetDetail: null,
isLoadingDatasetDetail: false,
loadingDatasetIds: [],
};typescript
// src/store/eval/slices/dataset/action.ts
import type { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';
import isEqual from 'fast-deep-equal';
import { mutate, useClientDataSWR } from '@/libs/swr';
import { agentEvalService } from '@/services/agentEval';
import type { EvalStore } from '@/store/eval/store';
import { datasetReducer, type DatasetDispatch } from './reducer';
const FETCH_DATASETS_KEY = 'FETCH_DATASETS';
const FETCH_DATASET_DETAIL_KEY = 'FETCH_DATASET_DETAIL';
export interface DatasetAction {
// SWR Hooks
useFetchDatasets: (benchmarkId?: string) => SWRResponse;
useFetchDatasetDetail: (id?: string) => SWRResponse;
// Refresh methods
refreshDatasets: (benchmarkId: string) => Promise<void>;
refreshDatasetDetail: (id: string) => Promise<void>;
// Mutations
createDataset: (params: any) => Promise<any>;
updateDataset: (params: any) => Promise<void>;
deleteDataset: (id: string, benchmarkId: string) => Promise<void>;
// Internal methods
internal_dispatchDataset: (payload: DatasetDispatch, benchmarkId: string) => void;
internal_updateDatasetLoading: (id: string, loading: boolean) => void;
}
export const createDatasetSlice: StateCreator<
EvalStore,
[['zustand/devtools', never]],
[],
DatasetAction
> = (set, get) => ({
// Fetch list with Map
useFetchDatasets: (benchmarkId) => {
return useClientDataSWR(
benchmarkId ? [FETCH_DATASETS_KEY, benchmarkId] : null,
() => agentEvalService.listDatasets(benchmarkId!),
{
onSuccess: (data: any) => {
set(
{
datasetMap: {
...get().datasetMap,
[benchmarkId!]: {
currentPage: 1,
hasMore: false,
isLoading: false,
items: data,
pageSize: data.length,
total: data.length,
},
},
},
false,
'useFetchDatasets/success',
);
},
},
);
},
// Fetch single item (for modal display)
useFetchDatasetDetail: (id) => {
return useClientDataSWR(
id ? [FETCH_DATASET_DETAIL_KEY, id] : null,
() => agentEvalService.getDataset(id!),
{
onSuccess: (data: any) => {
set(
{ datasetDetail: data, isLoadingDatasetDetail: false },
false,
'useFetchDatasetDetail/success',
);
},
},
);
},
refreshDatasets: async (benchmarkId) => {
await mutate([FETCH_DATASETS_KEY, benchmarkId]);
},
refreshDatasetDetail: async (id) => {
await mutate([FETCH_DATASET_DETAIL_KEY, id]);
},
// CREATE with optimistic update
createDataset: async (params) => {
const tmpId = Date.now().toString();
const { benchmarkId } = params;
get().internal_dispatchDataset(
{
type: 'addDataset',
value: { ...params, id: tmpId, createdAt: Date.now() } as any,
},
benchmarkId,
);
get().internal_updateDatasetLoading(tmpId, true);
try {
const result = await agentEvalService.createDataset(params);
await get().refreshDatasets(benchmarkId);
return result;
} finally {
get().internal_updateDatasetLoading(tmpId, false);
}
},
// UPDATE with optimistic update
updateDataset: async (params) => {
const { id, benchmarkId } = params;
get().internal_dispatchDataset(
{
type: 'updateDataset',
id,
value: params,
},
benchmarkId,
);
get().internal_updateDatasetLoading(id, true);
try {
await agentEvalService.updateDataset(params);
await get().refreshDatasets(benchmarkId);
} finally {
get().internal_updateDatasetLoading(id, false);
}
},
// DELETE with optimistic update
deleteDataset: async (id, benchmarkId) => {
get().internal_dispatchDataset(
{
type: 'deleteDataset',
id,
},
benchmarkId,
);
get().internal_updateDatasetLoading(id, true);
try {
await agentEvalService.deleteDataset(id);
await get().refreshDatasets(benchmarkId);
} finally {
get().internal_updateDatasetLoading(id, false);
}
},
// Internal - Dispatch to reducer
internal_dispatchDataset: (payload, benchmarkId) => {
const currentData = get().datasetMap[benchmarkId];
const nextItems = datasetReducer(currentData?.items, payload);
if (isEqual(nextItems, currentData?.items)) return;
set(
{
datasetMap: {
...get().datasetMap,
[benchmarkId]: {
...currentData,
currentPage: currentData?.currentPage ?? 1,
hasMore: currentData?.hasMore ?? false,
isLoading: false,
items: nextItems,
pageSize: currentData?.pageSize ?? nextItems.length,
total: currentData?.total ?? nextItems.length,
},
},
},
false,
`dispatchDataset/${payload.type}`,
);
},
// Internal - Update loading state
internal_updateDatasetLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingDatasetIds: [...state.loadingDatasetIds, id] };
}
return {
loadingDatasetIds: state.loadingDatasetIds.filter((i) => i !== id),
};
},
false,
'updateDatasetLoading',
);
},
});typescript
// src/store/eval/slices/dataset/initialState.ts
import type { Dataset } from '@/types/dataset';
export interface DatasetData {
currentPage: number;
hasMore: boolean;
isLoading: boolean;
items: Dataset[];
pageSize: number;
total: number;
}
export interface DatasetSliceState {
// 以benchmarkId为键的Map
datasetMap: Record<string, DatasetData>;
// 单个项的简单状态(只读,用于模态框)
datasetDetail: Dataset | null;
isLoadingDatasetDetail: boolean;
loadingDatasetIds: string[];
}
export const datasetInitialState: DatasetSliceState = {
datasetMap: {},
datasetDetail: null,
isLoadingDatasetDetail: false,
loadingDatasetIds: [],
};typescript
// src/store/eval/slices/dataset/action.ts
import type { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';
import isEqual from 'fast-deep-equal';
import { mutate, useClientDataSWR } from '@/libs/swr';
import { agentEvalService } from '@/services/agentEval';
import type { EvalStore } from '@/store/eval/store';
import { datasetReducer, type DatasetDispatch } from './reducer';
const FETCH_DATASETS_KEY = 'FETCH_DATASETS';
const FETCH_DATASET_DETAIL_KEY = 'FETCH_DATASET_DETAIL';
export interface DatasetAction {
// SWR钩子
useFetchDatasets: (benchmarkId?: string) => SWRResponse;
useFetchDatasetDetail: (id?: string) => SWRResponse;
// 刷新方法
refreshDatasets: (benchmarkId: string) => Promise<void>;
refreshDatasetDetail: (id: string) => Promise<void>;
// 变更操作
createDataset: (params: any) => Promise<any>;
updateDataset: (params: any) => Promise<void>;
deleteDataset: (id: string, benchmarkId: string) => Promise<void>;
// 内部方法
internal_dispatchDataset: (payload: DatasetDispatch, benchmarkId: string) => void;
internal_updateDatasetLoading: (id: string, loading: boolean) => void;
}
export const createDatasetSlice: StateCreator<
EvalStore,
[['zustand/devtools', never]],
[],
DatasetAction
> = (set, get) => ({
// 使用Map获取列表
useFetchDatasets: (benchmarkId) => {
return useClientDataSWR(
benchmarkId ? [FETCH_DATASETS_KEY, benchmarkId] : null,
() => agentEvalService.listDatasets(benchmarkId!),
{
onSuccess: (data: any) => {
set(
{
datasetMap: {
...get().datasetMap,
[benchmarkId!]: {
currentPage: 1,
hasMore: false,
isLoading: false,
items: data,
pageSize: data.length,
total: data.length,
},
},
},
false,
'useFetchDatasets/success',
);
},
},
);
},
// 获取单个项(用于模态框显示)
useFetchDatasetDetail: (id) => {
return useClientDataSWR(
id ? [FETCH_DATASET_DETAIL_KEY, id] : null,
() => agentEvalService.getDataset(id!),
{
onSuccess: (data: any) => {
set(
{ datasetDetail: data, isLoadingDatasetDetail: false },
false,
'useFetchDatasetDetail/success',
);
},
},
);
},
refreshDatasets: async (benchmarkId) => {
await mutate([FETCH_DATASETS_KEY, benchmarkId]);
},
refreshDatasetDetail: async (id) => {
await mutate([FETCH_DATASET_DETAIL_KEY, id]);
},
// 带乐观更新的创建操作
createDataset: async (params) => {
const tmpId = Date.now().toString();
const { benchmarkId } = params;
get().internal_dispatchDataset(
{
type: 'addDataset',
value: { ...params, id: tmpId, createdAt: Date.now() } as any,
},
benchmarkId,
);
get().internal_updateDatasetLoading(tmpId, true);
try {
const result = await agentEvalService.createDataset(params);
await get().refreshDatasets(benchmarkId);
return result;
} finally {
get().internal_updateDatasetLoading(tmpId, false);
}
},
// 带乐观更新的更新操作
updateDataset: async (params) => {
const { id, benchmarkId } = params;
get().internal_dispatchDataset(
{
type: 'updateDataset',
id,
value: params,
},
benchmarkId,
);
get().internal_updateDatasetLoading(id, true);
try {
await agentEvalService.updateDataset(params);
await get().refreshDatasets(benchmarkId);
} finally {
get().internal_updateDatasetLoading(id, false);
}
},
// 带乐观更新的删除操作
deleteDataset: async (id, benchmarkId) => {
get().internal_dispatchDataset(
{
type: 'deleteDataset',
id,
},
benchmarkId,
);
get().internal_updateDatasetLoading(id, true);
try {
await agentEvalService.deleteDataset(id);
await get().refreshDatasets(benchmarkId);
} finally {
get().internal_updateDatasetLoading(id, false);
}
},
// 内部方法 - 向reducer分发事件
internal_dispatchDataset: (payload, benchmarkId) => {
const currentData = get().datasetMap[benchmarkId];
const nextItems = datasetReducer(currentData?.items, payload);
if (isEqual(nextItems, currentData?.items)) return;
set(
{
datasetMap: {
...get().datasetMap,
[benchmarkId]: {
...currentData,
currentPage: currentData?.currentPage ?? 1,
hasMore: currentData?.hasMore ?? false,
isLoading: false,
items: nextItems,
pageSize: currentData?.pageSize ?? nextItems.length,
total: currentData?.total ?? nextItems.length,
},
},
},
false,
`dispatchDataset/${payload.type}`,
);
},
// 内部方法 - 更新加载状态
internal_updateDatasetLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingDatasetIds: [...state.loadingDatasetIds, id] };
}
return {
loadingDatasetIds: state.loadingDatasetIds.filter((i) => i !== id),
};
},
false,
'updateDatasetLoading',
);
},
});Step 3: Integrate into Store
步骤3:集成到Store
typescript
// src/store/eval/store.ts
import { createDatasetSlice, type DatasetAction } from './slices/dataset/action';
export type EvalStore = EvalStoreState &
BenchmarkAction &
DatasetAction & // Add here
RunAction;
const createStore: StateCreator<EvalStore, [['zustand/devtools', never]]> = (set, get, store) => ({
...initialState,
...createBenchmarkSlice(set, get, store),
...createDatasetSlice(set, get, store), // Add here
...createRunSlice(set, get, store),
});typescript
// src/store/eval/initialState.ts
import { datasetInitialState, type DatasetSliceState } from './slices/dataset/initialState';
export interface EvalStoreState extends BenchmarkSliceState, DatasetSliceState {
// ...
}
export const initialState: EvalStoreState = {
...benchmarkInitialState,
...datasetInitialState, // Add here
...runInitialState,
};typescript
// src/store/eval/store.ts
import { createDatasetSlice, type DatasetAction } from './slices/dataset/action';
export type EvalStore = EvalStoreState &
BenchmarkAction &
DatasetAction & // 在此处添加
RunAction;
const createStore: StateCreator<EvalStore, [['zustand/devtools', never]]> = (set, get, store) => ({
...initialState,
...createBenchmarkSlice(set, get, store),
...createDatasetSlice(set, get, store), // 在此处添加
...createRunSlice(set, get, store),
});typescript
// src/store/eval/initialState.ts
import { datasetInitialState, type DatasetSliceState } from './slices/dataset/initialState';
export interface EvalStoreState extends BenchmarkSliceState, DatasetSliceState {
// ...
}
export const initialState: EvalStoreState = {
...benchmarkInitialState,
...datasetInitialState, // 在此处添加
...runInitialState,
};Step 4: Create Selectors (Optional but Recommended)
步骤4:创建选择器(可选但推荐)
typescript
// src/store/eval/slices/dataset/selectors.ts
import type { EvalStore } from '@/store/eval/store';
export const datasetSelectors = {
getDatasetData: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId],
getDatasets: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId]?.items ?? [],
isLoadingDataset: (id: string) => (s: EvalStore) => s.loadingDatasetIds.includes(id),
};typescript
// src/store/eval/slices/dataset/selectors.ts
import type { EvalStore } from '@/store/eval/store';
export const datasetSelectors = {
getDatasetData: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId],
getDatasets: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId]?.items ?? [],
isLoadingDataset: (id: string) => (s: EvalStore) => s.loadingDatasetIds.includes(id),
};Step 5: Use in Component
步骤5:在组件中使用
typescript
// Component - List with Map
import { useEvalStore } from '@/store/eval';
import { datasetSelectors } from '@/store/eval/selectors';
const DatasetList = ({ benchmarkId }: { benchmarkId: string }) => {
const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
const datasets = useEvalStore(datasetSelectors.getDatasets(benchmarkId));
const datasetData = useEvalStore(datasetSelectors.getDatasetData(benchmarkId));
useFetchDatasets(benchmarkId);
if (datasetData?.isLoading) return <Loading />;
return (
<div>
<h2>Total: {datasetData?.total ?? 0}</h2>
<List data={datasets} />
</div>
);
};
// Component - Single item (for modal)
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
const isLoading = useEvalStore((s) => s.isLoadingDatasetDetail);
// Only fetch when modal is open
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return (
<Modal open={open}>
{isLoading ? <Loading /> : <div>{dataset?.name}</div>}
</Modal>
);
};typescript
// 组件 - 使用Map的列表
import { useEvalStore } from '@/store/eval';
import { datasetSelectors } from '@/store/eval/selectors';
const DatasetList = ({ benchmarkId }: { benchmarkId: string }) => {
const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
const datasets = useEvalStore(datasetSelectors.getDatasets(benchmarkId));
const datasetData = useEvalStore(datasetSelectors.getDatasetData(benchmarkId));
useFetchDatasets(benchmarkId);
if (datasetData?.isLoading) return <Loading />;
return (
<div>
<h2>总数: {datasetData?.total ?? 0}</h2>
<List data={datasets} />
</div>
);
};
// 组件 - 单个项(用于模态框)
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
const isLoading = useEvalStore((s) => s.isLoadingDatasetDetail);
// 仅在模态框打开时获取数据
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return (
<Modal open={open}>
{isLoading ? <Loading /> : <div>{dataset?.name}</div>}
</Modal>
);
};Common Patterns
常见模式
Pattern 1: List + Detail
模式1:列表 + 详情
typescript
// List with pagination
useFetchTestCases: (params) => {
const { datasetId, limit, offset } = params;
return useClientDataSWR(
datasetId ? [FETCH_TEST_CASES_KEY, datasetId, limit, offset] : null,
() => agentEvalService.listTestCases({ datasetId, limit, offset }),
{
onSuccess: (data: any) => {
set(
{
testCaseList: data.data,
testCaseTotal: data.total,
isLoadingTestCases: false,
},
false,
'useFetchTestCases/success',
);
},
},
);
};typescript
// 带分页的列表
useFetchTestCases: (params) => {
const { datasetId, limit, offset } = params;
return useClientDataSWR(
datasetId ? [FETCH_TEST_CASES_KEY, datasetId, limit, offset] : null,
() => agentEvalService.listTestCases({ datasetId, limit, offset }),
{
onSuccess: (data: any) => {
set(
{
testCaseList: data.data,
testCaseTotal: data.total,
isLoadingTestCases: false,
},
false,
'useFetchTestCases/success',
);
},
},
);
};Pattern 2: Dependent Fetching
模式2:依赖式获取
typescript
// Component
const BenchmarkDetail = () => {
const { benchmarkId } = useParams();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const benchmark = useEvalStore((s) => s.benchmarkDetail);
const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
const datasets = useEvalStore((s) => s.datasetList);
// Fetch benchmark first
useFetchBenchmarkDetail(benchmarkId);
// Then fetch datasets for this benchmark
useFetchDatasets(benchmarkId);
return <div>...</div>;
};typescript
// 组件
const BenchmarkDetail = () => {
const { benchmarkId } = useParams();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const benchmark = useEvalStore((s) => s.benchmarkDetail);
const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
const datasets = useEvalStore((s) => s.datasetList);
// 先获取基准数据
useFetchBenchmarkDetail(benchmarkId);
// 然后获取该基准对应的数据集
useFetchDatasets(benchmarkId);
return <div>...</div>;
};Pattern 3: Conditional Fetching
模式3:条件式获取
typescript
// Only fetch when modal is open
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
// Only fetch when open AND datasetId exists
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return <Modal open={open}>...</Modal>;
};typescript
// 仅在模态框打开时获取数据
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
// 仅在打开且datasetId存在时获取
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return <Modal open={open}>...</Modal>;
};Pattern 4: Refresh After Mutation
模式4:变更后刷新
typescript
// Store action
createDataset: async (params) => {
const result = await agentEvalService.createDataset(params);
// Refresh the list after creation
await get().refreshDatasets(params.benchmarkId);
return result;
};
deleteDataset: async (id, benchmarkId) => {
await agentEvalService.deleteDataset(id);
// Refresh the list after deletion
await get().refreshDatasets(benchmarkId);
};typescript
// Store action
createDataset: async (params) => {
const result = await agentEvalService.createDataset(params);
// 创建后刷新列表
await get().refreshDatasets(params.benchmarkId);
return result;
};
deleteDataset: async (id, benchmarkId) => {
await agentEvalService.deleteDataset(id);
// 删除后刷新列表
await get().refreshDatasets(benchmarkId);
};Migration Guide: useEffect → Store SWR
迁移指南:useEffect → Store SWR
Before (❌ Wrong)
迁移前(❌ 错误做法)
typescript
const TestCaseList = ({ datasetId }: Props) => {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result = await lambdaClient.agentEval.listTestCases.query({
datasetId,
});
setData(result.data);
} finally {
setLoading(false);
}
};
fetchData();
}, [datasetId]);
return <Table data={data} loading={loading} />;
};typescript
const TestCaseList = ({ datasetId }: Props) => {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result = await lambdaClient.agentEval.listTestCases.query({
datasetId,
});
setData(result.data);
} finally {
setLoading(false);
}
};
fetchData();
}, [datasetId]);
return <Table data={data} loading={loading} />;
};After (✅ Correct)
迁移后(✅ 正确做法)
typescript
// 1. Create service method
class AgentEvalService {
async listTestCases(params: { datasetId: string }) {
return lambdaClient.agentEval.listTestCases.query(params);
}
}
// 2. Create store slice
export const createTestCaseSlice: StateCreator<...> = (set) => ({
useFetchTestCases: (params) => {
return useClientDataSWR(
params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId] : null,
() => agentEvalService.listTestCases(params),
{
onSuccess: (data: any) => {
set(
{ testCaseList: data.data, isLoadingTestCases: false },
false,
'useFetchTestCases/success',
);
},
},
);
},
});
// 3. Use in component
const TestCaseList = ({ datasetId }: Props) => {
const useFetchTestCases = useEvalStore((s) => s.useFetchTestCases);
const data = useEvalStore((s) => s.testCaseList);
const loading = useEvalStore((s) => s.isLoadingTestCases);
useFetchTestCases({ datasetId });
return <Table data={data} loading={loading} />;
};typescript
// 1. 创建服务方法
class AgentEvalService {
async listTestCases(params: { datasetId: string }) {
return lambdaClient.agentEval.listTestCases.query(params);
}
}
// 2. 创建store切片
export const createTestCaseSlice: StateCreator<...> = (set) => ({
useFetchTestCases: (params) => {
return useClientDataSWR(
params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId] : null,
() => agentEvalService.listTestCases(params),
{
onSuccess: (data: any) => {
set(
{ testCaseList: data.data, isLoadingTestCases: false },
false,
'useFetchTestCases/success',
);
},
},
);
},
});
// 3. 在组件中使用
const TestCaseList = ({ datasetId }: Props) => {
const useFetchTestCases = useEvalStore((s) => s.useFetchTestCases);
const data = useEvalStore((s) => s.testCaseList);
const loading = useEvalStore((s) => s.isLoadingTestCases);
useFetchTestCases({ datasetId });
return <Table data={data} loading={loading} />;
};Best Practices
最佳实践
✅ DO
✅ 推荐做法
- Always use service layer - Never call lambdaClient directly in stores/components
- Use SWR hooks in stores - Not useEffect in components
- Clear naming - for hooks,
useFetchXxxfor cache invalidationrefreshXxx - Proper cache keys - Use constants, include parameters in array form
- Update state in onSuccess - Set loading states and data
- Refresh after mutations - Call refresh methods after create/update/delete
- Handle loading states - Provide loading indicators to users
- 始终使用Service层 - 绝不在store或组件中直接调用lambdaClient
- 在store中使用SWR钩子 - 不在组件中使用useEffect
- 清晰的命名 - 用于钩子,
useFetchXxx用于缓存失效refreshXxx - 合适的缓存键 - 使用常量,参数以数组形式包含
- 在onSuccess中更新状态 - 设置加载状态和数据
- 变更后刷新缓存 - 创建/更新/删除后调用刷新方法
- 处理加载状态 - 为用户提供加载指示器
❌ DON'T
❌ 禁止做法
- Don't use useEffect for data fetching
- Don't use useState for server data
- Don't call lambdaClient directly in components or stores
- Don't forget to refresh cache after mutations
- Don't duplicate state - Use store as single source of truth
- 不要使用useEffect进行数据获取
- 不要使用useState存储服务端数据
- 不要在组件或store中直接调用lambdaClient
- 变更后不要忘记刷新缓存
- 不要重复存储状态 - 使用store作为唯一可信源
Troubleshooting
故障排除
Problem: Data not loading
问题:数据未加载
Check:
- Is the hook being called?
useFetchXxx() - Is the key valid? (not null/undefined)
- Is the service method correct?
- Check browser network tab for API calls
检查项:
- 是否调用了钩子?
useFetchXxx() - 缓存键是否有效?(不为null/undefined)
- 服务方法是否正确?
- 检查浏览器网络标签页中的API调用
Problem: Data not refreshing after mutation
问题:变更后数据未刷新
Check:
- Did you call after mutation?
refreshXxx() - Is the cache key the same in both hook and refresh?
- Check devtools for state updates
检查项:
- 变更后是否调用了?
refreshXxx() - 钩子和刷新方法中的缓存键是否一致?
- 检查devtools中的状态更新
Problem: Loading state stuck
问题:加载状态卡住
Check:
- Is updating
onSuccess?isLoadingXxx: false - Is there an error in the API call?
- Check error boundary or console
检查项:
- 中是否更新了
onSuccess?isLoadingXxx: false - API调用是否出错?
- 检查错误边界或控制台
Summary Checklist
总结检查清单
When implementing new data fetching:
实现新的数据获取功能时:
Step 1: Data Structures
步骤1:数据结构
Seeskill for detailed patternsstore-data-structures
- Define types in :
@lobechat/types- Detail type (e.g., )
AgentEvalBenchmark - List item type (e.g., )
AgentEvalBenchmarkListItem
- Detail type (e.g.,
- Design state structure:
- List:
xxxList: XxxListItem[] - Detail:
xxxDetailMap: Record<string, Xxx> - Loading:
loadingXxxDetailIds: string[]
- List:
- Create reducer if optimistic updates needed
参考技能了解详细模式store-data-structures
- 在中定义类型:
@lobechat/types- 详情类型(例如)
AgentEvalBenchmark - 列表项类型(例如)
AgentEvalBenchmarkListItem
- 详情类型(例如
- 设计状态结构:
- 列表:
xxxList: XxxListItem[] - 详情:
xxxDetailMap: Record<string, Xxx> - 加载状态:
loadingXxxDetailIds: string[]
- 列表:
- 如果需要乐观更新则创建reducer
Step 2: Service Layer
步骤2:Service层
- Create service in
src/services/xxxService.ts - Add methods:
- - fetch list
listXxx() - - fetch detail
getXxx(id) - ,
createXxx(),updateXxx()- mutationsdeleteXxx()
-
- 在中创建服务
src/services/xxxService.ts - 添加方法:
- - 获取列表
listXxx() - - 获取详情
getXxx(id) - 、
createXxx()、updateXxx()- 变更操作deleteXxx()
-
Step 3: Store Actions
步骤3:Store Actions
- Create with state structure
initialState.ts - Create with:
action.ts- - list SWR hook
useFetchXxxList() - - detail SWR hook
useFetchXxxDetail(id) - ,
refreshXxxList()- cache invalidationrefreshXxxDetail(id) - CRUD methods calling service
- and
internal_dispatchif using reducerinternal_updateLoading
-
- Create (optional but recommended)
selectors.ts - Integrate slice into main store
- 创建定义状态结构
initialState.ts - 创建包含:
action.ts- - 列表SWR钩子
useFetchXxxList() - - 详情SWR钩子
useFetchXxxDetail(id) - 、
refreshXxxList()- 缓存失效方法refreshXxxDetail(id) - 调用服务的CRUD方法
- 如果使用reducer则添加和
internal_dispatchinternal_updateLoading
-
- 创建(可选但推荐)
selectors.ts - 将切片集成到主store
Step 4: Component Usage
步骤4:组件使用
- Use store hooks (NOT useEffect)
- List pages: access array
xxxList - Detail pages: access
xxxDetailMap[id] - Use loading states for UI feedback
Remember: Types → Service → Store (SWR + Reducer) → Component 🎯
- 使用store钩子(绝不使用useEffect)
- 列表页面:访问数组
xxxList - 详情页面:访问
xxxDetailMap[id] - 使用加载状态提供UI反馈
记住:类型定义 → Service层 → Store(SWR + Reducer)→ 组件 🎯
Key Architecture Patterns
核心架构模式
- Service Layer: Clean API abstraction ()
xxxService - Data Structures: List arrays + Detail maps (see skill)
store-data-structures - SWR Hooks: Automatic caching and revalidation ()
useFetchXxx - Cache Invalidation: Manual refresh methods ()
refreshXxx - Optimistic Updates: Update UI immediately, then sync with server
- Loading States: Per-item loading for better UX
- Service层:清晰的API抽象()
xxxService - 数据结构:列表数组 + 详情Map(参考技能)
store-data-structures - SWR钩子:自动缓存和重新验证()
useFetchXxx - 缓存失效:手动刷新方法()
refreshXxx - 乐观更新:立即更新UI,然后与服务端同步
- 加载状态:按项加载以提升用户体验
Related Skills
相关技能
- - How to structure List and Detail data in stores
store-data-structures - - General Zustand patterns and best practices
zustand
- - 如何在store中构建列表和详情数据
store-data-structures - - 通用Zustand模式和最佳做法
zustand