首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >2026年React数据获取的第六层:从自己写缓存到用React Query——减少100行代码的秘诀

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

作者头像
前端达人
发布2026-03-12 14:41:07
发布2026-03-12 14:41:07
300
举报
文章被收录于专栏:前端达人前端达人
image
image

image

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

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

从100行代码到10行代码

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

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

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

代码语言:javascript
复制
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做法:

代码语言:javascript
复制
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大问题

代码语言:javascript
复制
// 问题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解决的问题

代码语言:javascript
复制
// ✅ 问题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基础

安装和初始化

代码语言:javascript
复制
npm install @tanstack/react-query
代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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中最容易被误用的概念。

查询键的原则

代码语言:javascript
复制
// ❌ 错误:太通用
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']
});

查询键的失效模式

代码语言:javascript
复制
// 场景:用户修改了信息

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选项优雅地处理这个问题。

代码语言:javascript
复制
// 用户选择一个作者,然后加载他的文章
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。

代码语言:javascript
复制
// 完整的添加用户示例
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)

代码语言:javascript
复制
// 传统的"第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)

代码语言:javascript
复制
// "滚动加载更多"式分页
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 自己写的对比

代码行数对比

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

自己写:
├─ 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配置

代码语言:javascript
复制
// 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小时
});

总结:从自己写到用框架

学习曲线

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

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

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

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

何时用React Query

场景

建议

简单的一次性fetch

❌ 不需要

有缓存需求

✅ 必用

有分页需求

✅ 必用

需要乐观更新

✅ 推荐

大型应用

✅ 必用

频繁的数据更新

✅ 必用

React Query的一句话

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

最后的话

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

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

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

  • ✅ 减少项目代码量50-80%
  • ✅ 避免90%的缓存相关bug
  • ✅ 自动获得最佳实践
  • ✅ 有更多精力关注业务逻辑

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从100行代码到10行代码
  • 第一部分:为什么需要React Query
    • 自己写缓存的5大问题
    • React Query解决的问题
  • 第二部分:React Query基础
    • 安装和初始化
    • 你的第一个Query
  • 第三部分:查询键的艺术
    • 查询键的原则
    • 查询键的失效模式
  • 第四部分:依赖查询
  • 第五部分:使用useMutation修改数据
  • 第六部分:分页
    • 偏移分页(Offset-Based)
    • 无限滚动(Infinite Scroll)
  • 第七部分:React Query vs 自己写的对比
    • 代码行数对比
    • 功能对比
  • 第八部分:生产级的React Query配置
  • 总结:从自己写到用框架
    • 学习曲线
    • 何时用React Query
    • React Query的一句话
  • 最后的话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档