10.SWR 与 React Query:它们到底解决了什么问题? 副本

查看飞书原文 ↗4,842 字符
​
在现代前端开发中,获取和管理远端数据是构建用户界面的核心环节。当我们谈论 React 应用中的数据获取时,useEffect 和 useState 的组合是许多人首先想到的方案。然而,随着应用复杂度的提升,我们很快会发现这个“经典组合”显得力不从心。​
​
这正是 SWR 和 React Query (现已更名为 TanStack Query) 等库大放异彩的地方。它们并非简单地替换 fetch 或 axios,而是提供了一套精密的策略,专门用于管理“服务端状态 (Server State)”。为了理解它们的价值,我们首先需要回到问题的起点:传统数据获取方式究竟有什么痛点?​
​
一、起点:useEffect 与 useState 的困境​
​
我们来看一个典型的数据获取场景:在组件挂载时请求一篇文章数据,并处理加载和错误状态。​
​
import React, { useState, useEffect } from 'react';

function Article({ id }) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 确保组件挂载时才执行
    let isMounted = true;

    const fetchData = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(`/api/articles/${id}`);
        if (!response.ok) {
          throw new Error('网络请求失败');
        }
        const result = await response.json();
        if (isMounted) {
          setData(result);
        }
      } catch (e) {
        if (isMounted) {
          setError(e);
        }
      } finally {
        if (isMounted) {
          setIsLoading(false);
        }
      }
    };

    fetchData();

    // 清理函数,防止组件卸载后更新状态
    return () => {
      isMounted = false;
    };
  }, [id]); // 依赖 id,当 id 变化时重新请求

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>发生错误: {error.message}</div>;

  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </div>
  );
}
​
这段代码看起来功能完备,但其中隐藏着不少问题:​
​
1.
模板代码冗余 (Boilerplate):每个需要获取数据的组件,几乎都需要重复实现 data, isLoading, error 这三组状态及其管理逻辑。​
2.
缺乏缓存机制:如果用户切换到其他页面再切回来,即使 id 不变,组件也会重新挂载并再次发送相同的网络请求,造成资源浪费和不必要的等待。​
3.
数据陈旧 (Stale Data):一旦数据获取成功,它就静静地待在 data 状态里。如果此时数据库中的这篇文章被其他用户修改了,我们的界面将毫无察觉,展示着过时的数据。​
4.
竞态条件 (Race Conditions):当 id 快速变化时(例如,用户在文章列表间快速切换),前一个请求可能比后一个请求更晚返回,导致 setData 写入了错误(旧的)数据。虽然上面代码中的 isMounted 标志可以缓解一部分卸载后的问题,但并不能完全解决请求顺序错乱的竞态问题。​
5.
体验不佳:应用在后台重新激活(用户切换浏览器标签页回来)、或者断网后重连时,我们往往希望数据能自动刷新,以确保其新鲜度。手动实现这些逻辑会非常复杂。​
​
这些问题共同指向一个核心:**手动管理异步的服务端状态既繁琐又容易出错**。​
​
二、核心思想:将服务端数据视为需要同步的状态​
​
SWR 和 React Query 的设计哲学,是把从服务器获取的数据看作一种特殊的、需要与远端“同步”的状态,而不是一次性的操作。这与 React 管理的“客户端状态” (Client State) 有着本质区别。​
​
•
客户端状态:由用户直接操作,是同步且可预测的(例如,表单输入、开关状态)。​
•
服务端状态:来自远端,是异步的、不受我们完全控制的,并且可能随时被其他人修改。​
​
因此,对于服务端状态,我们需要的不是简单的“获取”,而是一整套**缓存、更新、同步**的机制。​
​
SWR 这个名字本身就是其核心策略的缩写:**Stale-While-Revalidate**。​
​
​
Stale-While-Revalidate:这是一种缓存策略。当请求数据时,库会立即从缓存中返回“陈旧 (Stale)”的数据(如果存在),让 UI 能够瞬时渲染。与此同时,它在后台发起一个“重新验证 (Revalidate)”的请求,待获取到最新数据后再悄无声息地更新 UI。​
​
​
这种策略兼顾了性能和数据一致性:用户能快速看到内容,同时又能保证内容最终会更新到最新。​
​
三、SWR / React Query 解决了哪些具体问题?​
​
基于上述核心思想,这些库为我们解决了 useEffect 方案中的所有痛点。​
​
1. 缓存与去重 (Caching & Deduplication)​
​
这是最基础也是最重要的功能。当你使用 useSWR 或 useQuery 并传入一个唯一的 key (通常是 URL 或一个数组) 时:​
​
•
首次请求:数据被获取并存入全局缓存。​
•
后续请求:如果其他组件使用相同的 key 请求数据,库会直接从缓存中返回数据,而不会发起重复的网络请求。这极大地减少了不必要的 API 调用。​
​
效果:应用加载更快,服务端压力更小。​
​
2. 状态管理简化 (Simplified State Management)​
​
我们不再需要手动管理 isLoading, error, data。库已经帮我们封装好了。​
​
使用 React Query 的例子:​
​
import { useQuery } from '@tanstack/react-query';

const fetchArticle = async (id) => {
  const response = await fetch(`/api/articles/${id}`);
  if (!response.ok) {
    throw new Error('网络请求失败');
  }
  return response.json();
};

function Article({ id }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['article', id], // 唯一的 key
    queryFn: () => fetchArticle(id),
  });

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>发生错误: {error.message}</div>;

  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </div>
  );
}
​
对比之前的 useEffect 版本,代码量大幅减少,逻辑也变得极为清晰。我们只需声明“需要什么数据 (queryKey)”和“如何获取 (queryFn)”,其余的加载、错误处理、数据存储都由库自动完成。​
​
3. 自动重新验证 (Automatic Revalidation)​
​
为了解决数据陈旧问题,这些库会在特定时机自动触发数据刷新,确保界面信息与服务端保持同步。默认的触发时机包括:​
​
•
窗口聚焦时 (Revalidate on Focus):用户切换到其他应用再切回来,数据会自动刷新。​
•
网络重连时 (Revalidate on Reconnect):设备从离线状态恢复在线,数据会自动刷新。​
•
组件挂载时 (Revalidate on Mount):组件重新挂载时,会检查缓存数据是否过期并决定是否刷新。​
•
可配置的轮询 (Optional Interval Polling):可以设置一个时间间隔,定时刷新数据(例如,股票价格、监控仪表盘)。​
​
效果:应用具有“实时感”,用户总能看到相对较新的数据,而开发者无需编写任何额外代码。​
​
4. 复杂场景的内置支持​
​
除了上述核心功能,这些库还内置了对许多高级场景的优雅支持:​
​
•
分页 (Pagination) 和无限滚动 (Infinite Loading):提供了 useInfiniteQuery 等专用 Hooks,能极大地简化加载“下一页”数据的逻辑和状态管理。​
•
乐观更新 (Optimistic Updates):在修改数据的操作中(如点赞、评论),可以先假设请求会成功并立即更新 UI,然后再向服务器发送请求。如果请求失败,UI 会自动回滚到原始状态。这带来了极致流畅的交互体验。​
•
请求重试 (Request Retries):当网络请求失败时,库可以自动进行有限次数的重试。​
​
结论​
​
SWR 和 React Query 并非简单的数据请求工具,它们是**专为现代 Web 应用设计的服务端状态管理器**。它们解决的核心问题,是从手动、繁琐、易错的 useEffect 数据获取模式,转向一种自动、高效、健壮的声明式数据同步模型。​
​
通过引入**缓存、自动重新验证、状态简化**等核心机制,它们帮助我们:​
​
•
提升开发效率:减少样板代码,专注于业务逻辑。​
•
改善用户体验:通过缓存和 Stale-While-Revalidate 策略让应用响应更快,通过自动刷新让数据更“新鲜”。​
•
增强应用健壮性:内置处理了竞态条件、错误重试、网络变化等复杂边界情况。​
​
在今天,对于任何需要与服务端进行数据交互的非小型 React 应用而言,采用 SWR 或 React Query 已经不是一个“选项”,而是一种能够显著提升项目质量和开发体验的最佳实践。​
​
本文暂未被其它文档引用