使用react-query与react-hook-forms时,获取数据致表单短暂为空
问题描述
我正在开发一个SPA待办事项应用,使用react-hook-forms处理待办事项的创建与编辑,使用react-query获取待办数据。
用户点击“保存”后,路径从create-todo切换至todo/:id,同时使当前useGetTodo查询失效;路径变化时,useGetTodo会从服务器获取待办数据。
问题在于:切换路径并重取数据后,表单会短暂为空,直到useGetTodo获取完成后才重新填充(该待办是之前创建的,字段内容完全一致,本不应出现空表单情况)。
相关代码
表单初始化代码
const {todo} = useGetTodo(); const getDefaultValues = useCallback(() => { return { name: todo.name, isDone: todo.isDone } }, [todo]); const form = useForm({ mode: 'all', defaultValues: useMemo(getDefaultValues, [getDefaultValues]), }); useEffect(() => { if (todo) { reset(getDefaultValues); } }, [reset, todo, getDefaultValues]);
useGetTodo实现代码
const {data, ...rest} = useQuery(['todo', todoId], async () => { if (todoId) { return await getTodo(todoId) } return null; })
react-query初始化代码
const queryClient = new QueryClient({ defaultOptions: { suspense: false, keepPreviousData: true, } })
保存时的mutation代码
const todoMutation = useMutation( ['todo'], getUpdatedTodo, {onSuccess: () => queryClient.invalidateQueries(['todo', todoId])} );
问题原因及解决方案
1. 路由切换时新查询无缓存兜底
从create-todo切换到todo/:id时,新路由的todoId对应全新的查询键['todo', newId],keepPreviousData仅对同一查询键的分页/滚动场景生效,对不同键的查询不适用。此时新查询初始状态下data为undefined,todo为空,导致表单默认值无法获取有效数据,出现短暂空白。
解决办法:
给useQuery的返回数据设置兜底默认值,避免todo为null/undefined时表单字段报错或为空:
const {data: todo = { name: '', isDone: false }, ...rest} = useQuery(['todo', todoId], async () => { if (todoId) { return await getTodo(todoId) } return { name: '', isDone: false }; })
2. 表单默认值初始化时机过早
useMemo(getDefaultValues, [getDefaultValues])在组件挂载时立即执行,此时若todo还未加载完成,getDefaultValues会返回{ name: undefined, isDone: undefined },导致表单初始为空。后续todo加载完成后才触发useEffect中的reset,中间存在空白期。
解决办法:
将defaultValues设为延迟执行的函数,让useForm在需要时才获取最新的todo值,同时简化冗余代码:
const form = useForm({ mode: 'all', defaultValues: () => ({ name: todo?.name || '', isDone: todo?.isDone || false }), }); // 可删除原有的getDefaultValues、useCallback、useMemo以及对应的useEffect
3. 失效查询未优先更新缓存
点击保存后直接执行invalidateQueries,会标记旧缓存为失效,新路由的查询需要重新请求服务器,等待期间表单无数据填充。
优化建议:
利用setQueryData在mutation成功后直接更新对应查询的缓存,让新路由的useGetTodo能立即拿到最新数据,再通过invalidateQueries做后台同步:
const todoMutation = useMutation( getUpdatedTodo, { onSuccess: (updatedTodo) => { // 先更新缓存,保证路由切换后能立即拿到数据 queryClient.setQueryData(['todo', updatedTodo.id], updatedTodo); // 后台同步最新数据 queryClient.invalidateQueries(['todo', updatedTodo.id]); } } );
内容的提问来源于stack exchange,提问作者noer

