标准答案

  1. 请求函数应和组件展示逻辑分离,统一处理 baseURL、鉴权头、错误格式和取消能力。
  2. 组件层需要表达 loading、empty、success、error、retry、refreshing 等状态,而不是只用一个布尔值。
  3. 缓存要区分服务端数据和客户端 UI 状态,列表、详情、分页和搜索通常需要不同 key。
  4. 错误处理要分层:接口错误转成稳定结构,页面决定如何提示,登录失效和权限错误可以走全局处理。
  5. 当出现重复请求、缓存失效、后台刷新和乐观更新需求时,应考虑 React Query / TanStack Query。

题目解析

这道题考的是工程组织能力。API 请求不是在 useEffect 里 fetch 一下就结束,还要考虑状态生命周期。

服务端数据和客户端状态的心智不同:服务端数据有过期、失效、重试和并发刷新问题。

请求层越统一,组件越容易只关心“数据能否展示”和“用户下一步能做什么”。

代码示例

下面用自定义 Hook 把列表请求状态收敛到一个明确的返回结构。

TSX
function useUsers() {
  const [state, setState] = useState<{
    status: 'idle' | 'loading' | 'success' | 'error'
    data: User[]
    error?: string
  }>({ status: 'idle', data: [] })

  useEffect(() => {
    const controller = new AbortController()

    setState(prev => ({ ...prev, status: 'loading', error: undefined }))
    fetchUsers({ signal: controller.signal })
      .then(data => setState({ status: 'success', data }))
      .catch(error => {
        if (error.name !== 'AbortError') {
          setState({ status: 'error', data: [], error: normalizeError(error) })
        }
      })

    return () => controller.abort()
  }, [])

  return state
}

常见误区

  • 每个组件都手写 fetch、loading、error,最后错误格式和重试行为不一致。
  • 把接口返回数据直接当作永久客户端状态,不考虑过期、刷新和缓存失效。
  • 请求取消和竞态没有处理,旧请求返回后覆盖了新页面状态。

作者信息