React-Query useQuery异步获取时跳过条目问题排查
类似Tinder应用的分页加载异常问题
问题背景
开发类似Tinder的应用,核心功能逻辑:
- 每次从数据库获取4条用户条目
- 用户逐个查看,点击点赞/点踩按钮触发异步函数将操作写入数据库
- 滑到当前获取的最后一条条目时,自动获取下一批4条数据
组件代码
export const HomePage: React.FC = () => { const { user } = useUser() const userId = user?.id const [current, setCurrent] = useState<number | null>(0) const [skip, setSkip] = useState(0) const [url, setUrl] = useState( `/api/swipes/get-users/?userId=${user?.id}&skip=${skip}` ) const getUsers = async (url: string) => { const { data } = await axios.get(url) return data } const { error, data, isLoading, refetch } = useQuery( ['userSwipeData', url], () => getUsers(url), { enabled: !!user?.id, } ) const handleRefetchUsers = () => { setCurrent(0) setUrl( `/api/swipes/get-users/?userId=${user?.id}&skip=${ skip + FETCH_USERS_PAGINATION_LIMIT }` ) refetch() setSkip(skip + FETCH_USERS_PAGINATION_LIMIT) } const handleSwipe = async (e: any) => { if (current === null || !data) return const { value } = e.target await handleSwipeType(value, data?.users[current].id) } const handleSwipeType = async (type: string, id: string) => { const values = { userSwipeOn: id, currentUser: userId, } // if (type === 'YES') { // if (current && data?.users[current]?.isMatch) { // await axios.post('/api/swipes/create-match', values) // alert('You have a match!') // } // await axios.post('/api/swipes/like', values) // } else { // await axios.post('/api/swipes/dislike', values) // } if (current === data.users.length - 1) { handleRefetchUsers() } else { setCurrent(current! + 1) } } if (isLoading || current === null) return <Box>Fetching users...</Box> if (error) return <Box>An error has occurred </Box> if (data && !data?.users.length) return <Box>No more users for now</Box> return ( <Box> <h1>Current User: {data?.users[current].email}</h1> <Flex> <button value="NO" onClick={handleSwipe}> NO </button> <button value="YES" onClick={handleSwipe}> YES </button> </Flex> </Box> ) }
当前问题
注释掉异步写入数据库的代码时,handleRefetchUsers()函数工作正常;但取消注释这些异步代码后,每次触发该函数都会跳过4条条目。
解决思路
问题根源
问题出在React状态更新的异步特性和闭包捕获旧值:
handleSwipeType里的异步请求(axios.post)会等待执行完成,这段时间内组件可能已经重新渲染,但函数内捕获的skip还是触发时的旧值。handleRefetchUsers直接使用skip + FETCH_USERS_PAGINATION_LIMIT更新状态,当异步操作完成后调用该函数,闭包中的skip是旧值,导致多次累加或跳步。
修复方案
1. 使用函数式更新获取最新状态
修改handleRefetchUsers,通过函数式更新获取最新的skip值,避免闭包捕获旧值:
const handleRefetchUsers = () => { setCurrent(0); // 用函数式更新拿到最新的skip状态 setSkip(prevSkip => { const newSkip = prevSkip + FETCH_USERS_PAGINATION_LIMIT; setUrl(`/api/swipes/get-users/?userId=${user?.id}&skip=${newSkip}`); refetch(); return newSkip; }); };
2. 提前保存状态,避免异步期间状态变化影响判断
在执行异步操作前,先判断并保存当前是否为最后一条用户,避免异步操作期间状态变化导致判断错误:
const handleSwipeType = async (type: string, id: string) => { const values = { userSwipeOn: id, currentUser: userId, }; // 提前保存是否为最后一条的判断结果 const isLastUser = current === data.users.length - 1; // 执行异步写入操作 if (type === 'YES') { if (current && data?.users[current]?.isMatch) { await axios.post('/api/swipes/create-match', values); alert('You have a match!'); } await axios.post('/api/swipes/like', values); } else { await axios.post('/api/swipes/dislike', values); } // 用提前保存的结果判断,不受异步期间状态变化影响 if (isLastUser) { handleRefetchUsers(); } else { // 同样用函数式更新获取最新的current值 setCurrent(prev => prev! + 1); } };
3. 优化useQuery调用,自动触发请求
因为useQuery的queryKey包含url,当url更新时,useQuery会自动重新发起请求,所以可以去掉手动调用refetch(),让useQuery自动处理:
const handleRefetchUsers = () => { setCurrent(0); setSkip(prevSkip => { const newSkip = prevSkip + FETCH_USERS_PAGINATION_LIMIT; setUrl(`/api/swipes/get-users/?userId=${user?.id}&skip=${newSkip}`); return newSkip; }); };
这样当url更新后,queryKey变化,useQuery会自动触发新的请求,无需手动调用refetch(),减少重复操作。
内容的提问来源于stack exchange,提问作者Ben Shekhtman
相关产品推荐
相关产品推荐

