React Query中onMutate内更新缓存后如何优雅执行DOM滚动操作?
更稳健的滚动实现方案
方案一:通过useLayoutEffect监听消息数据变化
乐观更新会修改React Query的缓存,触发messages.data更新和组件重渲染。useLayoutEffect会在DOM更新完成后同步执行,此时容器的scrollHeight已经包含了新增的消息,无需依赖定时器即可准确滚动到底部。
function Chat() { const containerRef = React.useRef(null); const messages = useQuery({ queryKey: ["messages"], queryFn: api.getMessages }); const createMessage = useMutation({ mutationFn: (message) => api.createMessage(message), onMutate: (message) => { optimisitcUpdate(messages, message); }, }); // 监听消息数据变化,DOM更新后立即滚动 React.useLayoutEffect(() => { const container = containerRef.current; if (container && messages.data) { container.scrollTo({ top: container.scrollHeight, behavior: "smooth" // 可选:添加平滑滚动效果 }); } }, [messages.data]); return ( <div id="messages" ref={containerRef}> {messages.data?.map((message) => ( <div key={message.uuid}>{message.body}</div> ))} </div> ); }
如果需要避免初始加载时自动滚动(仅在新增消息时触发),可以增加消息数量的判断:
const prevMessageCountRef = React.useRef(0); React.useLayoutEffect(() => { const container = containerRef.current; const currentCount = messages.data?.length || 0; // 仅当消息数量增加时执行滚动 if (container && currentCount > prevMessageCountRef.current) { container.scrollTo(0, container.scrollHeight); } prevMessageCountRef.current = currentCount; }, [messages.data]);
方案二:用flushSync强制同步DOM更新
如果你的乐观更新逻辑是直接修改React Query缓存(比如通过queryClient.setQueryData),可以用React.flushSync包裹乐观更新操作,强制DOM同步更新后再执行滚动:
import { flushSync } from 'react'; import { useQueryClient } from '@tanstack/react-query'; function Chat() { const containerRef = React.useRef(null); const queryClient = useQueryClient(); const messages = useQuery({ queryKey: ["messages"], queryFn: api.getMessages }); const createMessage = useMutation({ mutationFn: (message) => api.createMessage(message), onMutate: (newMessage) => { // 用flushSync包裹缓存更新,强制DOM同步更新 flushSync(() => { queryClient.setQueryData(["messages"], (oldData) => { return [...(oldData || []), newMessage]; }); }); // 此时DOM已更新,可直接滚动 const container = containerRef.current; container?.scrollTo(0, container.scrollHeight); }, }); return ( <div id="messages" ref={containerRef}> {messages.data?.map((message) => ( <div key={message.uuid}>{message.body}</div> ))} </div> ); }
方案对比
useLayoutEffect方案更通用,无需修改乐观更新逻辑,完全贴合React的渲染生命周期,是最推荐的稳健方案。flushSync方案适合需要在乐观更新后立即执行滚动的场景,但需要确保乐观更新操作被包裹在flushSync内,对代码侵入性稍高。
内容的提问来源于stack exchange,提问作者Eliya Cohen
相关产品推荐
相关产品推荐

