React Query:相同Payload调用setState时不触发API请求的问题
解决React Query相同筛选条件下点击应用按钮不触发API的问题
问题场景
将项目从redux-saga迁移到React Query时,交易历史页面的筛选功能存在以下问题:
- 初始加载或切换不同筛选条件时,React Query能正常触发API请求;
- 当用户选择与当前相同的筛选条件并点击应用按钮时,由于queryKey依赖的payload内容未发生变化,React Query不会重新发起API请求;
- 即使通过扩展对象强制修改payload的引用,问题仍未解决。
需要实现:无论筛选条件是否与当前一致,点击应用按钮都能触发API获取最新数据。
可行解决方案
方案1:直接调用refetch方法
从useQuery的返回值中解构出refetch函数,在点击应用按钮时主动调用,强制触发API请求。
修改后代码示例:
import './App.css'; import TodoCard from './components/TodoCard'; import { todo } from './types/todo'; import { QueryClient, useQuery } from '@tanstack/react-query'; import React from 'react'; const queryClient = new QueryClient(); function App2() { const [payload, setState] = React.useState({ type: 1 }); // 从queryKey中获取参数,避免闭包导致的旧值问题 const fetchTodos = async ({ queryKey }) => { const [_, currentPayload] = queryKey; console.log("State received is at fetchtodos", currentPayload); const res = await fetch( `https://jsonplaceholder.typicode.com/todos?_page=${currentPayload.type}` ); return res.json(); }; const { isFetching, data, status, error, refetch // 解构refetch方法 } = useQuery({ queryKey: ['todos', payload], queryFn: fetchTodos, staleTime: 0, gcTime: Infinity, placeholderData: [], }); const content = data && data.map((todo: todo) => ( <TodoCard key={todo.id} todo={todo} /> )); if (status === 'error') { return <p>Error: {error.message}</p>; } return ( <div className='app'> <button onClick={() => { setState((state) => ({ ...state, type: 2 })); refetch(); // 强制触发重新请求 }}>Press</button> {isFetching && <h3>Loading...</h3>} {!isFetching && content}{' '} </div> ); } export default App2;
方案2:在queryKey中加入刷新计数器
维护一个额外的状态refreshCount,每次点击应用按钮时递增该计数器,并将其加入queryKey。即使payload未变化,queryKey也会因计数器变化而更新,触发React Query重新请求。
修改后代码示例:
import './App.css'; import TodoCard from './components/TodoCard'; import { todo } from './types/todo'; import { QueryClient, useQuery } from '@tanstack/react-query'; import React from 'react'; const queryClient = new QueryClient(); function App2() { const [payload, setState] = React.useState({ type: 1 }); const [refreshCount, setRefreshCount] = React.useState(0); // 新增刷新计数器 const fetchTodos = async ({ queryKey }) => { const [_, currentPayload] = queryKey; console.log("State received is at fetchtodos", currentPayload); const res = await fetch( `https://jsonplaceholder.typicode.com/todos?_page=${currentPayload.type}` ); return res.json(); }; const { isFetching, data, status, error, } = useQuery({ queryKey: ['todos', payload, refreshCount], // 加入计数器到queryKey queryFn: fetchTodos, staleTime: 0, gcTime: Infinity, placeholderData: [], }); const content = data && data.map((todo: todo) => ( <TodoCard key={todo.id} todo={todo} /> )); if (status === 'error') { return <p>Error: {error.message}</p>; } return ( <div className='app'> <button onClick={() => { setState((state) => ({ ...state, type: 2 })); setRefreshCount(prev => prev + 1); // 递增计数器 }}>Press</button> {isFetching && <h3>Loading...</h3>} {!isFetching && content}{' '} </div> ); } export default App2;
方案3:使用invalidateQueries标记数据过期
通过useQueryClient获取queryClient实例,在点击按钮时调用invalidateQueries,让React Query标记对应查询为过期,自动重新发起请求。
修改后代码示例:
import './App.css'; import TodoCard from './components/TodoCard'; import { todo } from './types/todo'; import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; import React from 'react'; const queryClient = new QueryClient(); function App2() { const [payload, setState] = React.useState({ type: 1 }); const queryClient = useQueryClient(); // 获取queryClient实例 const fetchTodos = async ({ queryKey }) => { const [_, currentPayload] = queryKey; console.log("State received is at fetchtodos", currentPayload); const res = await fetch( `https://jsonplaceholder.typicode.com/todos?_page=${currentPayload.type}` ); return res.json(); }; const { isFetching, data, status, error, } = useQuery({ queryKey: ['todos', payload], queryFn: fetchTodos, staleTime: 0, gcTime: Infinity, placeholderData: [], }); const content = data && data.map((todo: todo) => ( <TodoCard key={todo.id} todo={todo} /> )); if (status === 'error') { return <p>Error: {error.message}</p>; } return ( <div className='app'> <button onClick={() => { setState((state) => ({ ...state, type: 2 })); // 标记对应查询为过期,触发重新请求 queryClient.invalidateQueries({ queryKey: ['todos', payload] }); }}>Press</button> {isFetching && <h3>Loading...</h3>} {!isFetching && content}{' '} </div> ); } export default App2;
注意事项
- 建议从
queryKey中获取请求参数,避免直接使用外部state导致的闭包问题(原代码中fetchTodos直接引用payload可能会拿到旧值); - 三种方案可根据场景选择:
refetch适合明确手动触发的场景,计数器方案适合需要自动关联状态变化的场景,invalidateQueries适合批量失效多个查询的场景。
内容的提问来源于stack exchange,提问作者Divya Prakash
相关产品推荐
相关产品推荐

