如何用React Query在不频繁发送Mutation的情况下更新展示数据?
近期开始使用React Query管理数据获取与缓存,但遇到了难题:如何在不频繁发送mutation的情况下更新展示的数据?
例如,在组件顶部通过查询获取用户列表,将所有用户展示在简易卡片中。此前使用React的useState时,会为输入框绑定handleFieldChange(id_user, field, value)函数,该函数仅更新指定用户的状态,点击保存按钮时再将更新后的用户数据发送至服务器。
以下是原useState实现的示例代码:
// react import { useState, useEffect } from 'react'; // api import { getUsers, updateUser } from '../api/users'; const UserCardGrid: React.FC = () => { const [users, setUsers] = useState([]); // handlers const handleGetUsers = async () => { const { data } = await getUsers(); setUsers(data); }; const handleFieldChange = (id_user: number, key: string, value: any) => { const newUsers = users.map((user: any) => { if (user.id_user === id_user) { return { ...user, [key]: value, }; } return user; }); setUsers(newUsers); }; const handleUpdateUser = async (id_user: number) => { const user = users.find((user: any) => user.id_user === id_user); await updateUser(id_user, user); }; // effects useEffect(() => { handleGetUsers(); }, []); return ( <div className='grid'> {users.map((user: any) => ( <div key={user.id_user}> <input key={user.id_user} type='text' value={user.username} onChange={(e) => handleFieldChange(user.id_user, 'username', e.target.value) } /> <button onClick={() => handleUpdateUser(user.id_user)}>Update</button> </div> ))} </div> ); }; export default UserCardGrid;
但使用React Query时,无法直接沿用这种方式。若要更新展示的文本,似乎每次输入都需更新缓存或发送useMutation请求。知道可以使用useMutation和乐观更新,但即使用防抖机制,每次输入都发送mutation也并不合理。
以下是React Query版本的待实现代码:
// api import { useQuery, useMutation } from '@tanstack/react-query'; import { getUsers, updateUser } from '../api/users'; const UserCardGrid: React.FC = () => { const usersQ = useQuery(['users'], getUsers); const userM = useMutation(updateUser); // handlers const handleFieldChange = (id_user: number, key: string, value: any) => { // HOW COULD I IMPLEMENT THIS? }; const handleUpdateUser = async (id_user: number) => { userM.mutate(id_user, usersQ.data.find((user: any) => user.id_user === id_user)); }; return ( <div className='grid'> {usersQ.data?.map((user: any) => ( <div key={user.id_user}> <input key={user.id_user} type='text' value={user.username} onChange={(e) => handleFieldChange(user.id_user, 'username', e.target.value) } /> <button onClick={() => handleUpdateUser(user.id_user)}>Update</button> </div> ))} </div> ); }; export default UserCardGrid;
希望能自由修改用户数据,仅在点击保存按钮时再更新服务器数据。可以将用户数据存入state,但又不想放弃React Query返回页面时自动重新获取数据的特性。
方案一:本地状态 + React Query缓存同步
核心思路是用useState维护当前编辑的用户数据,同时监听React Query返回的data,当缓存更新(比如返回页面自动重新获取)时,同步更新本地状态。这样既保留了React Query的缓存和自动刷新特性,又能在本地自由修改,仅保存时发送请求。
实现代码:
import { useState, useEffect } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { getUsers, updateUser } from '../api/users'; const UserCardGrid: React.FC = () => { const usersQ = useQuery(['users'], getUsers); const [localUsers, setLocalUsers] = useState<any[]>([]); const updateUserMutation = useMutation({ mutationFn: updateUser, // 成功后重新获取用户列表,保证缓存与服务器一致 onSuccess: () => { usersQ.refetch(); }, }); // 当React Query缓存更新时,同步到本地状态 useEffect(() => { if (usersQ.data) { setLocalUsers(usersQ.data); } }, [usersQ.data]); const handleFieldChange = (id_user: number, key: string, value: any) => { setLocalUsers(prev => prev.map(user => user.id_user === id_user ? { ...user, [key]: value } : user )); }; const handleUpdateUser = (id_user: number) => { const updatedUser = localUsers.find(user => user.id_user === id_user); if (updatedUser) { updateUserMutation.mutate({ id_user, data: updatedUser }); } }; if (usersQ.isLoading) return <div>Loading...</div>; if (usersQ.isError) return <div>Error loading users</div>; return ( <div className='grid'> {localUsers.map((user) => ( <div key={user.id_user}> <input type='text' value={user.username} onChange={(e) => handleFieldChange(user.id_user, 'username', e.target.value) } /> <button onClick={() => handleUpdateUser(user.id_user)} disabled={updateUserMutation.isLoading} > {updateUserMutation.isLoading ? 'Saving...' : 'Update'} </button> </div> ))} </div> ); }; export default UserCardGrid;
方案二:直接操作React Query缓存(临时更新)
如果不想维护额外的本地状态,可以直接用React Query的setQueryData方法临时更新缓存中的数据,点击保存时发送Mutation,成功后重新获取数据保证一致性,失败则回滚缓存到之前的状态。
实现代码:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getUsers, updateUser } from '../api/users'; const UserCardGrid: React.FC = () => { const queryClient = useQueryClient(); const usersQ = useQuery(['users'], getUsers); const updateUserMutation = useMutation({ mutationFn: updateUser, // 乐观更新:发送请求前先更新缓存 onMutate: async (updatedUser) => { // 取消当前可能的请求,避免竞态问题 await queryClient.cancelQueries(['users']); // 保存当前缓存状态,用于失败回滚 const previousUsers = queryClient.getQueryData(['users']); // 更新缓存中的用户数据 queryClient.setQueryData(['users'], (oldUsers: any[]) => oldUsers.map(user => user.id_user === updatedUser.id_user ? updatedUser.data : user ) ); // 返回回滚所需的上下文 return { previousUsers }; }, // 请求失败时回滚缓存 onError: (_err, _newUser, context) => { if (context?.previousUsers) { queryClient.setQueryData(['users'], context.previousUsers); } }, // 请求完成后,重新获取用户列表保证数据一致 onSettled: () => { queryClient.invalidateQueries(['users']); }, }); const handleFieldChange = (id_user: number, key: string, value: any) => { // 直接更新React Query缓存中的数据 queryClient.setQueryData(['users'], (oldUsers: any[]) => oldUsers.map(user => user.id_user === id_user ? { ...user, [key]: value } : user ) ); }; const handleUpdateUser = (id_user: number) => { const updatedUser = usersQ.data?.find(user => user.id_user === id_user); if (updatedUser) { updateUserMutation.mutate({ id_user, data: updatedUser }); } }; if (usersQ.isLoading) return <div>Loading...</div>; if (usersQ.isError) return <div>Error loading users</div>; return ( <div className='grid'> {usersQ.data?.map((user) => ( <div key={user.id_user}> <input type='text' value={user.username} onChange={(e) => handleFieldChange(user.id_user, 'username', e.target.value) } /> <button onClick={() => handleUpdateUser(user.id_user)} disabled={updateUserMutation.isLoading} > {updateUserMutation.isLoading ? 'Saving...' : 'Update'} </button> </div> ))} </div> ); }; export default UserCardGrid;
内容的提问来源于stack exchange,提问作者AnonymZ

