You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 02:05:25