2026年React数据获取的第六层:从自己写缓存到用React Query——减少100行代码的秘诀


image

前置阅读: 这是本系列的最后一篇核心文章,强烈建议先读完前五篇:

本篇是对前面所有内容的整合------我们用React Query将之前手写的所有复杂逻辑浓缩成几行代码。

从100行代码到10行代码

某个创业团队的应用,用户管理界面需要:

go 复制代码
需求清单:
✅ 加载用户列表
✅ 缓存用户数据
✅ 用户修改时更新缓存
✅ 支持分页
✅ 处理加载/错误状态
✅ 自动重试失败的请求
✅ 检测到window失焦时后台更新
✅ 防止重复请求

初级做法(自己写所有逻辑):

go 复制代码
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const cache = useRef(newMap());

  useEffect(() => {
    let isMounted = true;
    const controller = new AbortController();

    const loadUsers = async () => {
      const cacheKey = `page:${page}`;
      
      if (cache.current.has(cacheKey)) {
        setUsers(cache.current.get(cacheKey));
        return;
      }

      try {
        setLoading(true);
        const response = await fetch(`/api/users?page=${page}`, {
          signal: controller.signal
        });
        const data = await response.json();
        
        if (isMounted) {
          setUsers(data);
          cache.current.set(cacheKey, data);
        }
      } catch (err) {
        if (isMounted && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    loadUsers();

    return() => {
      isMounted = false;
      controller.abort();
    };
  }, [page]);

// 添加用户
const addUser = async (userData) => {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(userData)
    });
    const newUser = await response.json();
    
    // 手动更新缓存
    setUsers([...users, newUser]);
    
    return newUser;
  };

// 处理window失焦
  useEffect(() => {
    const handleFocus = () => {
      // 重新加载数据
      loadUsers();
    };
    
    window.addEventListener('focus', handleFocus);
    return() =>window.removeEventListener('focus', handleFocus);
  }, []);

if (loading) return<div>加载中...</div>;
if (error) return<div>错误: {error}</div>;

return (
    <div>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <button onClick={() => setPage(p => p + 1)}>下一页</button>
    </div>
  );
}

代码行数:80+ 行,且容易出bug

用React Query做法:

go 复制代码
function UserList() {
const [page, setPage] = useState(1);

const { data: users, isLoading, error } = useQuery({
    queryKey: ['users', page],
    queryFn: () => fetch(`/api/users?page=${page}`).then(r => r.json()),
  });

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

return (
    <div>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <button onClick={() => setPage(p => p + 1)}>下一页</button>
    </div>
  );
}

代码行数:15 行,而且自动处理了:

  • ✅ 缓存

  • ✅ window失焦后台更新

  • ✅ 自动重试

  • ✅ 防止重复请求

  • ✅ 错误处理

代码减少:80行 → 15行(减少81%)

这不仅仅是少写代码,更重要的是少维护代码。React Query处理了所有的边界情况。

第一部分:为什么需要React Query

自己写缓存的5大问题

go 复制代码
// 问题1:缓存永不过期
const cache = newMap();
cache.set('users', data);  // 什么时候删除?

// 问题2:缓存失效复杂
function updateUser(userId, updates) {
// 需要手动清除相关缓存
  cache.delete('users');
  cache.delete(`user:${userId}`);
  cache.delete('users:list');
// 还有其他地方用到这数据吗?我不确定...
}

// 问题3:防止重复请求需要自己写
let userRequestPromise = null;
function getUser(id) {
if (userRequestPromise) return userRequestPromise;
  userRequestPromise = fetch(`/api/users/${id}`);
return userRequestPromise;
}
// 这样写每个API都要重复...

// 问题4:window失焦后更新需要监听
useEffect(() => {
const handleFocus = () => {
    // 重新加载?但要避免不必要的请求...
  };
window.addEventListener('focus', handleFocus);
return() =>window.removeEventListener('focus', handleFocus);
}, []);

// 问题5:分页缓存很容易混乱
// page=1的数据被page=2的请求覆盖了吗?
// 用户返回到page=1,需要重新加载吗?

React Query解决的问题

go 复制代码
// ✅ 问题1:内置TTL机制
const queryClient = new QueryClient({
defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // 5分钟
      cacheTime: 10 * 60 * 1000// 10分钟后清除
    }
  }
});

// ✅ 问题2:智能缓存失效
useMutation({
mutationFn: updateUser,
onSuccess: () => {
    // 一行代码,自动更新相关缓存
    queryClient.invalidateQueries({ queryKey: ['users'] });
  }
});

// ✅ 问题3:自动防重复
// 多个组件同时请求相同数据 → 只发一个请求

// ✅ 问题4:自动window focus重新获取
// 配置一行代码就自动处理

// ✅ 问题5:分页缓存自动隔离
useQuery({ queryKey: ['users', page] });  // 不同page自动独立缓存

第二部分:React Query基础

安装和初始化

go 复制代码
npm install @tanstack/react-query
go 复制代码
// main.jsx
import { QueryClient, QueryClientProvider } from'@tanstack/react-query';

const queryClient = new QueryClient({
defaultOptions: {
    queries: {
      // ⏱️ 数据新鲜度时间
      // 这段时间内,如果有缓存就直接用,不发新请求
      staleTime: 5 * 60 * 1000,  // 5分钟
      
      // 💾 缓存保留时间
      // 超过这个时间,缓存被删除
      gcTime: 10 * 60 * 1000,  // 10分钟(新版本叫gcTime,旧版本叫cacheTime)
      
      // 🔄 失败重试
      // 失败的请求自动重试次数
      retry: 1,
      
      // ⚡ 是否在window获焦时重新获取
      refetchOnWindowFocus: true,
      
      // 📡 是否在重新mount时重新获取
      refetchOnMount: true,
    },
  },
});

exportdefaultfunction App() {
return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

你的第一个Query

go 复制代码
// hooks/useUsers.js
import { useQuery } from'@tanstack/react-query';

exportfunction useUsers(page = 1) {
return useQuery({
    // 查询键:用于缓存标识和失效
    // 不同的queryKey = 不同的缓存条目
    queryKey: ['users', { page }],
    
    // 查询函数:实际的API调用
    // 接收一个signal用于cancellation
    queryFn: async ({ signal }) => {
      const response = await fetch(`/api/users?page=${page}`, { signal });
      
      if (!response.ok) {
        thrownewError(`API error: ${response.status}`);
      }
      
      return response.json();
    },
    
    // staleTime可以按查询覆盖全局设置
    staleTime: 5 * 60 * 1000,
  });
}

// components/UserList.jsx
import { useUsers } from'../hooks/useUsers';

function UserList() {
const [page, setPage] = useState(1);

// 返回值包含所有你需要的状态
const {
    data: users,        // 实际的数据
    isLoading,          // 首次加载中
    isPending,          // 加载中(包括background refetch)
    isFetching,         // 正在后台获取
    error,              // 错误对象
    status,             // 'pending' | 'error' | 'success'
    fetchStatus,        // 'idle' | 'fetching' | 'paused'
  } = useUsers(page);

if (isLoading) {
    return<div className="loading">加载用户列表中...</div>;
  }

if (error) {
    return<div className="error">错误: {error.message}</div>;
  }

// 后台更新时显示指示器
if (isFetching && !isLoading) {
    return<div className="bg-sync">💫 更新中...</div>;
  }

return (
    <div>
      <ul>
        {users?.map(user => (
          <li key={user.id}>
            {user.name}
            {user.email && <span> ({user.email})</span>}
          </li>
        ))}
      </ul>

      <div className="pagination">
        <button
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          ← 上一页
        </button>
        
        <span>第 {page} 页</span>
        
        <button onClick={() => setPage(p => p + 1)}>
          下一页 →
        </button>
      </div>
    </div>
  );
}

第三部分:查询键的艺术

查询键决定了缓存的范围。这是React Query中最容易被误用的概念。

查询键的原则

go 复制代码
// ❌ 错误:太通用
useQuery({
queryKey: ['data'],  // 太宽泛,什么数据?
queryFn: () => fetch('/api/users').then(r => r.json())
});

// ✅ 正确:具体和层级化
useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json())
});

// ❌ 错误:把参数写成字符串
useQuery({
queryKey: [`users:${userId}`],  // 还是字符串拼接,丑
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
});

// ✅ 正确:参数作为数组元素
useQuery({
queryKey: ['users', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
});

// ✅ 更好:参数作为对象(便于失效时匹配)
useQuery({
queryKey: ['users', { id: userId, include: 'posts' }],
queryFn: ({ queryKey }) => {
    const [, { id, include }] = queryKey;
    return fetch(`/api/users/${id}?include=${include}`).then(r => r.json());
  }
});

// ✅ 层级化键:用于精确失效
useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetch(`/api/users/${userId}/posts`).then(r => r.json())
});

// 当用户更新时:
queryClient.invalidateQueries({
queryKey: ['users', userId]  // 会失效 ['users', userId] 和 ['users', userId, 'posts']
});

查询键的失效模式

go 复制代码
// 场景:用户修改了信息

const updateUser = useMutation({
mutationFn: (updates) => patch(`/api/users/${userId}`, updates),
onSuccess: () => {
    // 模式1:精确失效
    queryClient.invalidateQueries({
      queryKey: ['users', userId]
    });

    // 模式2:使用前缀失效(所有相关的缓存)
    queryClient.invalidateQueries({
      queryKey: ['users'],
      exact: false// 匹配所有以['users']开头的键
    });

    // 模式3:使用predicate失效
    queryClient.invalidateQueries({
      predicate: (query) => {
        return query.queryKey[0] === 'users';  // 手动指定条件
      }
    });
  }
});

第四部分:依赖查询

某些数据依赖于其他数据。React Query用enabled选项优雅地处理这个问题。

go 复制代码
// 用户选择一个作者,然后加载他的文章
function AuthorPosts({ authorId }) {
// 第一步:加载作者信息
const {
    data: author,
    isLoading: authorLoading,
  } = useQuery({
    queryKey: ['authors', authorId],
    queryFn: () => fetch(`/api/authors/${authorId}`).then(r => r.json()),
  });

// 第二步:只有当作者加载完毕,才加载他的文章
const {
    data: posts,
    isLoading: postsLoading,
  } = useQuery({
    queryKey: ['authors', authorId, 'posts'],
    queryFn: () => fetch(`/api/authors/${authorId}/posts`).then(r => r.json()),
    enabled: !!author,  // ✅ 关键:只在author存在时执行
  });

if (authorLoading) return<div>加载作者...</div>;
if (!author) return<div>作者不存在</div>;

if (postsLoading) return<div>加载文章...</div>;

return (
    <div>
      <h1>{author.name}</h1>
      <ul>
        {posts?.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

第五部分:使用useMutation修改数据

Reading是Query,Writing是Mutation。

go 复制代码
// 完整的添加用户示例
function AddUserForm() {
const queryClient = useQueryClient();
const [formData, setFormData] = useState({ name: '', email: '' });

// 定义mutation
const mutation = useMutation({
    // mutationFn:实际的API调用
    mutationFn: async (userData) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      });

      if (!response.ok) {
        thrownewError('添加用户失败');
      }

      return response.json();
    },

    // onMutate:mutation开始前运行(用于乐观更新)
    onMutate: async (userData) => {
      // 取消任何pending的查询
      await queryClient.cancelQueries({ queryKey: ['users'] });

      // 保存旧数据(用于回滚)
      const previousUsers = queryClient.getQueryData(['users']);

      // 乐观更新(立即显示新用户)
      queryClient.setQueryData(['users'], (old) => {
        return [...(old || []), { ...userData, id: Date.now() }];
      });

      return { previousUsers };
    },

    // onSuccess:mutation成功
    onSuccess: (newUser) => {
      // 重新获取用户列表以获取完整数据(id、创建时间等)
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },

    // onError:mutation失败
    onError: (error, variables, context) => {
      // 回滚乐观更新
      if (context?.previousUsers) {
        queryClient.setQueryData(['users'], context.previousUsers);
      }

      // 显示错误提示
      console.error('失败:', error.message);
    },

    // onSettled:无论成功或失败都运行
    onSettled: () => {
      // 清空表单
      setFormData({ name: '', email: '' });
    },
  });

const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(formData);
  };

return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="用户名"
        required
      />

      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="邮箱"
        required
      />

      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? '添加中...' : '添加用户'}
      </button>

      {/* 状态反馈 */}
      {mutation.isPending && <p>⏳ 正在添加...</p>}
      {mutation.isSuccess && <p>✅ 添加成功!</p>}
      {mutation.isError && (
        <p style={{ color: 'red' }}>❌ 添加失败: {mutation.error.message}</p>
      )}
    </form>
  );
}

第六部分:分页

偏移分页(Offset-Based)

go 复制代码
// 传统的"第1页、第2页"分页
function PaginatedUserList() {
const [page, setPage] = useState(1);
const PAGE_SIZE = 20;

const {
    data,
    isLoading,
    isPreviousData,  // 上一页数据是否还在显示
  } = useQuery({
    queryKey: ['users', page],
    queryFn: () =>
      fetch(`/api/users?page=${page}&limit=${PAGE_SIZE}`).then(r => r.json()),
    // keepPreviousData在加载时显示旧数据
    placeholderData: (previousData) => previousData,
  });

const maxPages = Math.ceil((data?.total || 0) / PAGE_SIZE);

return (
    <div>
      {isPreviousData && <div className="stale">(显示旧数据)</div>}

      <ul>
        {data?.users?.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>

      <div className="pagination">
        <button onClick={() => setPage(1)} disabled={page === 1}>
          首页
        </button>

        <button
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          上一页
        </button>

        <span>第 {page} / {maxPages} 页</span>

        <button
          onClick={() => setPage(p => p + 1)}
          disabled={page >= maxPages}
        >
          下一页
        </button>

        <button onClick={() => setPage(maxPages)} disabled={page >= maxPages}>
          末页
        </button>
      </div>
    </div>
  );
}

无限滚动(Infinite Scroll)

go 复制代码
// "滚动加载更多"式分页
import { useInfiniteQuery } from'@tanstack/react-query';
import { useInView } from'react-intersection-observer';

function InfiniteUserList() {
// useInfiniteQuery:为无限滚动设计
const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['users:infinite'],
    
    // queryFn接收pageParam
    queryFn: async ({ pageParam = 0 }) => {
      const response = await fetch(
        `/api/users?cursor=${pageParam}&limit=20`
      );
      return response.json();
    },

    // 指定如何获取下一页的cursor
    getNextPageParam: (lastPage) => {
      return lastPage.nextCursor;  // undefined = 没有更多数据
    },

    // 初始的cursor值
    initialPageParam: 0,
  });

// 使用Intersection Observer检测用户滚动
const { ref, inView } = useInView();

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

if (status === 'pending') return<div>加载中...</div>;
if (status === 'error') return<div>加载失败</div>;

return (
    <div>
      {/* 渲染所有页面的数据 */}
      {data?.pages.map((page, pageIndex) => (
        <div key={pageIndex}>
          {page.users?.map(user => (
            <div key={user.id} className="user-item">
              {user.name}
            </div>
          ))}
        </div>
      ))}

      {/* 加载触发器 */}
      <div ref={ref} className="load-more-trigger">
        {isFetchingNextPage ? (
          '⏳ 加载更多中...'
        ) : hasNextPage ? (
          '👇 滚动加载更多'
        ) : (
          '✅ 没有更多数据了'
        )}
      </div>
    </div>
  );
}

第七部分:React Query vs 自己写的对比

代码行数对比

go 复制代码
任务:完整的用户管理(读+写+缓存+分页)

自己写:
├─ useEffect获取数据       30行
├─ 缓存管理               40行
├─ 失效和更新             50行
├─ 分页逻辑               30行
├─ 加载/错误状态          20行
└─ 总计                   170行 ❌

React Query:
├─ useQuery              5行
├─ useMutation           10行
├─ useInfiniteQuery      8行
└─ 总计                  23行 ✅

代码减少:170 → 23 = 减少87%

功能对比

功能 自己写 React Query
缓存 ⚠️ 复杂 ✅ 内置
缓存失效 ⚠️ 容易漏 ✅ 自动
防重复请求 ⚠️ 要写 ✅ 自动
Window focus更新 ⚠️ 要写 ✅ 内置
乐观更新 ❌ 很难 ✅ 容易
分页 ⚠️ 易出bug ✅ 成熟
加载状态 ⚠️ 要管理 ✅ 自动
错误处理 ⚠️ 要自己处理 ✅ 内置
重试逻辑 ❌ 没有 ✅ 内置
DevTools调试 ❌ 没有 ✅ 官方工具

第八部分:生产级的React Query配置

go 复制代码
// react-query-config.js
import { QueryClient } from'@tanstack/react-query';
import { createSyncStoragePersister } from'@tanstack/query-sync-storage-persister';
import { persistQueryClient } from'@tanstack/react-query-persist-client';

exportconst queryClient = new QueryClient({
defaultOptions: {
    queries: {
      // 数据新鲜度
      staleTime: 5 * 60 * 1000,  // 5分钟内认为数据新鲜
      gcTime: 10 * 60 * 1000,    // 10分钟后清除未使用的缓存

      // 重试策略
      retry: (failureCount, error) => {
        // 4xx错误不重试
        if (error.status >= 400 && error.status < 500) {
          returnfalse;
        }
        // 5xx错误重试,最多3次
        return failureCount < 3;
      },
      retryDelay: (attemptIndex) => {
        // 指数退避:100ms, 200ms, 400ms
        returnMath.min(1000 * 2 ** attemptIndex, 30000);
      },

      // 自动更新
      refetchOnWindowFocus: true,
      refetchOnMount: 'stale',  // 仅当数据过期时
      refetchOnReconnect: true,  // 网络恢复时
    },
  },
});

// 可选:持久化缓存到localStorage
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
});

persistQueryClient({
  queryClient,
persister: localStoragePersister,
maxAge: 24 * 60 * 60 * 1000,  // 24小时
});

总结:从自己写到用框架

学习曲线

go 复制代码
简单性:
自己写所有 ← → 用React Query
❌ 简单    ✅ 更简单

功能完整性:
自己写      ← → 用React Query
⚠️ 容易漏   ✅ 完整

可维护性:
自己写      ← → 用React Query
❌ 很难     ✅ 容易

性能:
自己写      ← → 用React Query
❌ 容易优化失败  ✅ 优化好的

何时用React Query

场景 建议
简单的一次性fetch ❌ 不需要
有缓存需求 ✅ 必用
有分页需求 ✅ 必用
需要乐观更新 ✅ 推荐
大型应用 ✅ 必用
频繁的数据更新 ✅ 必用

React Query的一句话

React Query是数据同步层,让你的React应用自动和服务器保持同步,不用手写缓存逻辑。

最后的话

这一篇是一个重要的分水岭------从"自己管理所有逻辑"到"用框架做正确的事"。

很多初级开发者被困在自己写缓存的泥沼里,而不知道React Query这样的库能简化90%的工作。

掌握了这一篇的内容,你就能:

  • ✅ 减少项目代码量50-80%

  • ✅ 避免90%的缓存相关bug

  • ✅ 自动获得最佳实践

  • ✅ 有更多精力关注业务逻辑

点赞、分享、评论支持,下一篇见!

相关推荐
2501_948122632 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 通知设置实现
javascript·react native·react.js·游戏·ecmascript·harmonyos
—Qeyser2 小时前
Flutter 生命周期完全指南:从出生到死亡的全过程
前端·javascript·flutter
2501_948122632 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 帮助中心实现
javascript·react native·react.js·游戏·ecmascript·harmonyos
YAY_tyy2 小时前
Turfjs 性能优化:大数据量地理要素处理技巧
前端·3d·arcgis·cesium·turfjs
hhcccchh2 小时前
学习vue第十二天 Vue开发工具链指南:从手工作坊到现代化工厂
前端·vue.js·学习
Yeats_Liao2 小时前
模型选型指南:7B、67B与MoE架构的业务适用性对比
前端·人工智能·神经网络·机器学习·架构·deep learning
念念不忘 必有回响2 小时前
Vue页面布局与路由映射实战:RouterView嵌套及动态组件生成详解
前端·javascript·vue.js
冰暮流星2 小时前
javascript数据类型转换-转换为数字型
开发语言·前端·javascript
—Qeyser2 小时前
Flutter StatelessWidget 完全指南:构建高效的静态界面
前端·flutter