1. 基础设施:类型与 Query Key 工厂

最佳实践:不要在 Hook 里手写字符串 key,使用工厂模式统一管理。

// src/features/tasks/queries.ts
import { keepPreviousData, queryOptions } from '@tanstack/react-query'
 
// --- Types (定义清晰的接口) ---
export interface Task {
  id: string
  title: string
  status: 'todo' | 'in-progress' | 'done'
  assignee?: string
  updatedAt: string
}
 
export interface TaskFilters {
  status?: string
  page?: number
}
 
// --- Query Key Factory (核心:单一真值来源) ---
// 解决了 "忘记这个 key 是什么" 和 "失效时不确定影响范围" 的问题
export const taskKeys = {
  all: ['tasks'] as const,
  lists: () => [...taskKeys.all, 'list'] as const,
  list: (filters: TaskFilters) => [...taskKeys.lists(), { ...filters }] as const,
  details: () => [...taskKeys.all, 'detail'] as const,
  detail: (id: string) => [...taskKeys.details(), id] as const,
}

2. 读操作 Hook:关注数据转换与防抖

最佳实践:利用 select 进行数据清洗,利用 placeholderData 提升列表体验。

// src/features/tasks/useTasks.ts
import { useQuery } from '@tanstack/react-query'
import { taskKeys, Task, TaskFilters } from './queries'
import { fetchTasks } from './api' // 假设这是纯粹的 fetcher
 
export const useTasks = (filters: TaskFilters) => {
  return useQuery({
    // 1. 使用 Key Factory 生成 Key
    queryKey: taskKeys.list(filters),
    
    // 2. QueryFn 应该只负责请求,不处理业务逻辑
    queryFn: () => fetchTasks(filters),
 
    // 3. 最佳实践:placeholderData
    // 在切换分页或筛选时,保留上一次的数据直到新数据到来,避免 loading 闪烁
    placeholderData: keepPreviousData,
 
    // 4. 最佳实践:staleTime
    // 服务端数据多久被认为是“陈旧”的?默认是0。
    // 设置合理的 staleTime 可以减少后台静默请求的频率
    staleTime: 1000 * 60 * 1, // 1分钟
 
    // 5. 最佳实践:select (数据转换层)
    // 这里的逻辑只会在 data 变化时运行。
    // 它可以把后端生硬的字段转为前端友好的结构,实现逻辑解耦。
    select: (data) => {
      return {
        raw: data,
        // 例如:预计算统计数据,或者处理时间格式
        totalCount: data.length,
        hasCompletedTasks: data.some(t => t.status === 'done'),
        // 甚至可以返回一个 Map 以便 O(1) 查找
        tasksMap: new Map(data.map(t => [t.id, t]))
      }
    }
  })
}

3. 写操作 Hook:解耦 UI 与乐观更新

最佳实践:Mutation 不决定义 UI 行为(不弹窗),但处理复杂的缓存一致性逻辑。

// src/features/tasks/useUpdateTask.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { taskKeys, Task } from './queries'
import { updateTaskApi } from './api'
 
// 定义 Mutation 接收的变量
interface UpdateTaskVariables {
  id: string
  data: Partial<Task>
}
 
export const useUpdateTask = () => {
  const queryClient = useQueryClient()
 
  return useMutation({
    mutationFn: ({ id, data }: UpdateTaskVariables) => updateTaskApi(id, data),
 
    // --- 核心:乐观更新 (Optimistic Updates) ---
    // 这是高级 Hook 必须掌握的,极大提升用户体验
    onMutate: async ({ id, data: newStats }) => {
      // A. 取消正在进行的查询,防止旧数据覆盖新数据
      await queryClient.cancelQueries({ queryKey: taskKeys.detail(id) })
      await queryClient.cancelQueries({ queryKey: taskKeys.lists() })
 
      // B. 获取当前的快照(用于回滚)
      const previousTask = queryClient.getQueryData<Task>(taskKeys.detail(id))
 
      // C. 乐观地更新缓存 (立即修改 UI,不等服务器响应)
      // 1. 更新详情
      if (previousTask) {
        queryClient.setQueryData(taskKeys.detail(id), { ...previousTask, ...newStats })
      }
      
      // 2. (可选) 更新列表中的该项
      queryClient.setQueriesData({ queryKey: taskKeys.lists() }, (old: Task[] | undefined) => {
        if (!old) return []
        return old.map(t => t.id === id ? { ...t, ...newStats } : t)
      })
 
      // D. 返回 context 供 onError 使用
      return { previousTask }
    },
 
    // 如果失败,回滚数据
    onError: (err, newTodo, context) => {
      if (context?.previousTask) {
        queryClient.setQueryData(taskKeys.detail(newTodo.id), context.previousTask)
      }
    },
 
    // --- 核心:解耦 UI ---
    // 这里**不要**写 showSuccessMessage()
    // 这里只处理 "数据一致性"
    onSettled: (data, error, variables) => {
      // 无论成功失败,都让相关查询失效,确保获取服务器最新状态
      queryClient.invalidateQueries({ queryKey: taskKeys.detail(variables.id) })
      queryClient.invalidateQueries({ queryKey: taskKeys.lists() })
    }
  })
}

4. UI 层调用:策略注入

最佳实践:在组件中决定如何反馈(Toast/Redirect),而不是在 Hook 中。

// src/components/TaskBoard.tsx
import { useTasks } from '@/features/tasks/useTasks'
import { useUpdateTask } from '@/features/tasks/useUpdateTask'
import { toast } from '@/components/ui/toast' // 假设的 UI 库
 
export const TaskBoard = () => {
  const [filter, setFilter] = useState({ page: 1 })
  
  // 使用 Read Hook
  // data 已经是经过 select 转换过的结构
  const { data, isPending, isPlaceholderData } = useTasks(filter)
  
  // 使用 Write Hook
  const updateMutation = useUpdateTask()
 
  const handleMarkDone = (taskId: string) => {
    updateMutation.mutate(
      { id: taskId, data: { status: 'done' } },
      {
        // ✨✨ 关键点:UI 逻辑在这里注入 ✨✨
        onSuccess: () => {
          toast.success("任务状态已更新!")
          // 这里甚至可以做路由跳转
        },
        onError: (error) => {
          toast.error(`更新失败: ${error.message}`)
        }
      }
    )
  }
 
  if (isPending) return <div>Loading...</div>
 
  return (
    <div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
      <h1>任务列表 (总数: {data?.totalCount})</h1>
      {data?.raw.map(task => (
        <TaskCard key={task.id} task={task} onToggle={() => handleMarkDone(task.id)} />
      ))}
    </div>
  )
}