掘金文章归档 · 第 13 篇 React Hooks · IntersectionObserver

自定义 hook 实现监测元素是否在可视窗口、用户是否向上滚动监听

useBottom + useScrollupListener,流式输出场景的"回到底部"按钮

原文作者:随意_(掘金) | juejin.cn/post/7517889147052539913 | 标签:前端

0背景

最近在写大模型流式返回的时候,默认是拼接文字直接滚动到底部的。对于初始化的时候历史内容过多,则需要展示"滚动到底部"的按钮。此 hook 就是监听设置的元素是否在可视区域,不在的展示滚动到底部按钮。

1实现:useBottom

import { useRef, useState, useEffect } from 'react';

export function useBottom(chatBottomRef: any) {
  const [showGoBottom, setShowGoBottom] = useState(false);
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        // 当参考元素完全不在视口中时显示按钮
        entries.forEach((entry) => {
          setShowGoBottom(!entry.isIntersecting);
        });
      },
      {
        root: null, // 使用浏览器视口作为根元素
        threshold: 0.1, // 当元素至少有10%可见时认为可见
        rootMargin: '0px'
      }
    );

    // 开始观察参考元素
    if (chatBottomRef.current) {
      observer.observe(chatBottomRef.current);
    }

    // 组件卸载时停止观察
    return () => {
      if (chatBottomRef.current) {
        observer.unobserve(chatBottomRef.current);
      }
    };
  }, [chatBottomRef]);

  return {
    showGoBottom
  };
}

2使用方法

const { showGoBottom } = useBottom(chatBottomRef);
<div
  className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5"
  ref={containterRef}
>

  {/* 底部滚动 */}
  <div ref={chatBottomRef} className="mb-40 h-[1px]"></div>
</div>
{/* 滚动动画 */}
{showGoBottom && (
  <GoBottom streaming={streaming} go={resetBottom}></GoBottom>
)}
截图占位:流式对话中历史内容较长时,底部出现"回到底部"按钮

3检测用户是否向上滚动 hook

import { useRef, useState, useEffect } from 'react';

export function useScrollupListener(elementRef) {
  const isScrollingUp = useRef(false);
  const lastScrollTop = useRef(0);

  useEffect(() => {
    const target = elementRef?.current || window;

    const handleScroll = () => {
      const scrollTop = elementRef
        ? target.scrollTop
        : window.scrollY || document.documentElement.scrollTop;

      // 判断是否向上滚动
      if (scrollTop < lastScrollTop.current) {
        isScrollingUp.current = true;
      }

      lastScrollTop.current = scrollTop;
    };

    target.addEventListener('scroll', handleScroll);

    return () => {
      target.removeEventListener('scroll', handleScroll);
    };
  }, [elementRef]);

  return { isScrollingUp };
}
hook核心机制典型用途
useBottomIntersectionObserver 监听底部占位元素,不可见时置 showGoBottom流式输出时按需展示"回到底部"按钮
useScrollupListenerscroll 事件对比 lastScrollTop,向上滚时置 isScrollingUp判断用户是否在回看历史、暂停自动滚动
📌 滚动容器内放一个 1px 高的底部占位元素,让 IntersectionObserver 以视口为根判断其是否可见,即可低成本实现"是否在底部"的检测。