- 前端
- 开发工具
【免费下载链接】beautiful-react-hooks
🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥
本篇指南围绕 beautiful-react-hooks 中的useInfiniteScrollHook 展开:它接收一个指向任意 HTML 元素的 ref,返回一个回调设置函数,由你在其中编写"加载更多"的业务逻辑,而滚动监听、触底判断、延迟触发与卸载清理等琐碎工作全部由 Hook 内部完成。读完本文,你将掌握该 Hook 的标准用法、delay参数的行为细节,以及它从事件绑定到触底判定的完整源码实现链路。
一、useInfiniteScroll 解决什么问题
根据官方文档 docs/useInfiniteScroll.md 的说明,useInfiniteScroll接收一个 HTML Element 引用,并返回一个便于为该特定元素处理无限滚动(infinite scroll)的函数。其设计目标可以归纳为三点:
- 为目标元素自动添加无限滚动所需的事件监听器,免去手写
addEventListener/removeEventListener样板代码; - 在组件卸载时负责清理事件监听器,降低应用中出现内存泄漏的风险;
- 通过直观、易用的接口,简化无限滚动业务逻辑的实现。
与基于window滚动位置做分页的方案不同,它针对的是任意指定容器元素自身的滚动(scroll事件),因此特别适合"页面中某个固定高度、内部可滚动的列表区域"这类场景——这也是官方示例采用maxHeight: 250, overflow: 'scroll'容器演示的原因。
二、安装与按路径导入
beautiful-react-hooks采用子路径导出(subpath exports)方式发布,每个 Hook 都可以独立按路径导入。以 package.json 中的exports字段为例,./useInfiniteScroll对应:
"./useInfiniteScroll": { "import": "./dist/esm/useInfiniteScroll.js", "require": "./dist/useInfiniteScroll.js", "types": "./dist/useInfiniteScroll.d.ts" }即同时提供 ESM、CommonJS 两种入口及 TypeScript 类型声明。安装后按下述方式导入即可:
npm install beautiful-react-hooksimport useInfiniteScroll from 'beautiful-react-hooks/useInfiniteScroll';这种"按需按路径导入"的方式意味着你不需要从包根部引入整个库,打包工具可以只解析useInfiniteScroll这一条依赖链。
三、完整示例:可滚动列表 + 模拟分页加载
下面是官方文档给出的完整示例(使用了 antd 的List、Alert、Typography组件,可替换为任意列表实现),演示了"初始渲染 40 条数据,滚动到底部后延迟拉取下一批数据"的典型流程:
import { useState, useRef } from 'react'; import { Alert, List, Typography } from 'antd'; import useInfiniteScroll from 'beautiful-react-hooks/useInfiniteScroll'; const generateRandomNo = () => Math.floor(Math.random() * 11) const initialData = Array.from({ length: 40 }).map(generateRandomNo) /** * Fake fetch, resolves an array of random numbers * @param items * @returns {Promise<unknown>} */ const fetchMock = (items = 10) => new Promise((resolve) => { setTimeout(() => { const data = Array.from({ length: items }).map(generateRandomNo) resolve(data) }, 1000) }) /** * Uses fetchMock to mimic an inifinite loading * @returns {JSX.Element} * @constructor */ const TestComponent = () => { const targetElementRef = useRef(); const onInfiniteScroll = useInfiniteScroll(targetElementRef); const [isFetching, setIsFetching] = useState(false) const [data, setData] = useState(initialData) onInfiniteScroll(() => { if (!isFetching) { setIsFetching(true) fetchMock() .then((next) => setData([...data, ...next])) .finally(() => setIsFetching(false)) } }) return ( <DisplayDemo title="useInfiniteScroll"> <div style={{ maxHeight: 250, overflow: 'scroll' }} ref={targetElementRef}> <div style={{ height: 500, position: 'relative' }}> <Alert type="info" message="Scroll to load more content" /> <List bordered dataSource={data} renderItem={(_, item) => ( <List.Item> <Typography.Text mark>mock item no: {item}</Typography.Text> </List.Item> )} /> {isFetching && ( <div style={{ opacity: 0.6, textAlign: 'center', marginBottom: 20 }}> Loading next data... </div> )} </div> </div> </DisplayDemo> ); };示例中有三个关键约定值得注意:
- ref 必须传给
useInfiniteScroll作为第一个参数,滚动监听会绑定到该 ref 指向的 DOM 元素上(示例中是那个maxHeight: 250、overflow: 'scroll'的容器); - 返回值的用法是"设置回调"而非"返回回调":
onInfiniteScroll(yourCallback)的语义是把yourCallback注册为触底时执行的处理器,这一点与useState的 setter 类似但不会触发组件重新渲染(原因见下文源码分析); - 防重复请求要自己处理:示例中用
isFetching标志位在回调内部做守卫。官方文档明确提醒:useInfiniteScroll本身不替你节流/防抖业务逻辑(它只对触底事件内置了一个可配置的delay),分页式的节流/防抖应由你自行控制,以保证应用行为完全符合你的预期。
四、源码实现剖析
4.1 函数签名与参数
源码位于 src/useInfiniteScroll.ts,核心签名如下:
const useInfiniteScroll = <TElement extends HTMLElement>(ref: RefObject<TElement>, delay = 300) => {ref: RefObject<TElement>:目标元素的 ref,泛型约束为任意HTMLElement子类,所以div、section、ul等均可作为滚动容器;delay?: number:可选,默认300ms。触底后并不是立即执行回调,而是先清掉未决的定时器、再启动一个delay毫秒的setTimeout,最终才调用你注册的回调——本质上是内置的防抖窗口。如果你的数据加载很快或想立即触发,可显式传入更小的值(甚至0)。
4.2 ref 合法性校验
Hook 开头对 ref 做了防御性检查:
if (ref && !safeHasOwnProperty(ref, 'current')) { throw new Error('Unable to assign any scroll event to the given ref') }它通过 src/shared/safeHasOwnProperty.ts 判断传入对象是否拥有current属性(即形如{ current: ... }的 ref 对象)。如果你传入的不是useRef()的返回值而是别的对象,会在渲染期直接抛错,而不是把错误推迟到运行时静默失败。
4.3 滚动监听:useEvent + passive 监听器 + 自动清理
事件绑定委托给库内的 useEvent 完成:
const onScroll = useEvent<UIEvent, TElement>(ref, 'scroll', { passive: true })useEvent 在useEffect中对target.current执行addEventListener('scroll', cb, { passive: true }),并在 effect 清理函数中对称地removeEventListener。这对应了文档中"组件卸载时清理监听器、减少内存泄漏风险"的承诺:卸载或target.current变化时,旧监听必然被移除。
两个实现细节值得注意:
{ passive: true }:scroll 事件被声明为 passive,浏览器因此可以不受事件回调阻塞而继续滚动,这对长列表滚动流畅性很重要。源码中event.preventDefault()一行处于注释状态,与 passive 选项保持一致;event.stopPropagation():每次滚动触发且元素满足触底条件判断流程时,会调用stopPropagation()。从源码结构看,这是为了防止该容器的 scroll 事件继续向冒泡路径上的祖先节点传播,避免与父级滚动逻辑(如同时使用useWindowScroll)相互干扰。
4.4 触底判定:1px 容差公式
滚动回调内部的核心判定只有四行:
const el = target as HTMLDivElement if (el) { const isBottom = Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) < 1 // ... }这是滚动容器"是否已到底部"的经典公式:scrollHeight(内容总高)−clientHeight(可视区高)−scrollTop(已滚动距离)即为"距底剩余像素"。这里没有用=== 0而是< 1,是出于亚像素/取整误差的容差考虑——部分浏览器滚动到最底部时该差值可能落在(0, 1)之间,严格相等判断会漏掉最后一次触底。
4.5 防抖窗口与延迟触发
确认触底后,Hook 并没有立刻执行回调,而是走一个"先清后设"的定时器模式(见 src/useInfiniteScroll.ts):
if (isBottom && isFunction(onScrollEnd?.current)) { clearTimeout(timeoutRef.current) timeoutRef.current = setTimeout(() => { if (onScrollEnd.current && isFunction(onScrollEnd.current)) { onScrollEnd.current() } clearTimeout(timeoutRef.current) }, delay) }- 每次触底滚动事件都先
clearTimeout再重新计时,因此快速连续触底滚动时,只有在停止"重新触底"delay(默认 300ms)后回调才真正执行一次——这正是内置防抖; - 定时器触发前会再次校验
onScrollEnd.current是否仍是合法函数(借助 src/shared/isFunction.ts),防止延迟期间组件状态变化导致调用到非法值。
4.6 回调如何"无重渲染"地被替换:createHandlerSetter
useInfiniteScroll通过工厂函数 createHandlerSetter 拿到[handlerRef, setHandler],并把setHandler作为 Hook 的返回值暴露给使用方:
const [onScrollEnd, setOnScrollEnd] = createHandlerSetter<unknown>() // ... return setOnScrollEndcreateHandlerSetter 的注释明确写道:"设置回调 ref不会强制组件重新渲染",它只把新函数写入handlerRef.current,并对非函数入参抛错。这解释了第三节的用法约定:onInfiniteScroll(fn)每轮渲染重新注册一次最新闭包(拿到最新的data、isFetching),但由于只写 ref、不触发 state 更新,注册动作本身是零渲染开销的。类型层面,该返回值对应 src/shared/types.ts 中定义的:
export type CallbackSetter<TArgs> = (nextCallback: SomeCallback<TArgs>) => void4.7 一条完整的调用链
把上述环节串起来,一次"触底触发"的完整链路是:
- 容器滚动 → 浏览器派发
scroll事件(passive 监听,不阻塞滚动); - useEvent 中的包装监听器把事件转发给
handler.current(即useInfiniteScroll注册的滚动回调); - 滚动回调计算
scrollHeight - clientHeight - scrollTop,差值绝对值< 1判定触底; clearTimeout+setTimeout(delay)重建防抖窗口;- 窗口结束且
onScrollEnd.current仍是函数时,调用你通过onInfiniteScroll(fn)注册的fn。
五、适用场景与边界
官方文档的 "Mastering the hook" 部分给出了明确的使用边界,这里完整保留:
✅ 何时使用(When to use)
- 用它来抽象你自己在应用中实现无限滚动业务逻辑所需的代码,简化各页面中该功能的落地。
🛑 不要做什么(What not to do)
- 不要用这个 Hook 去防抖或节流你的函数。如果你实现的是"分页式"的无限滚动,这类防抖/节流最好由你自行处理,以确保应用的行为完全符合你的预期。
结合源码可以补充几条实际边界:
- 触发源是容器元素自身的 scroll 事件,滚动
window/document而容器未滚动时不会触发; - 内置防抖窗口固定作用于"触底 → 执行回调"这一段,
delay只影响这段延迟,不会帮你合并"请求中再次触底"的情况——重复请求守卫(如示例中的isFetching)仍需业务侧实现; - 触底判定基于容器的
scrollHeight/clientHeight/scrollTop,若内容高度小于可视区高度(尚未撑满),公式可能恒为触底状态,注意配合内容最小高度或数据量。
六、TypeScript 类型声明
官方文档 docs/useInfiniteScroll.md 的 Types 一节给出的完整声明为:
import { type RefObject } from 'react'; /** * Accepts an HTML Element ref, then returns a function that allows you to handle the infinite * scroll for that specific element. */ declare const useInfiniteScroll: <TElement extends HTMLElement>(ref: RefObject<TElement>, delay?: number) => import("./shared/types").CallbackSetter<unknown>; export default useInfiniteScroll;可以从中读出三件事:ref是泛型化的RefObject<TElement>(TElement extends HTMLElement);delay是可选的第二参数(源码中默认值为 300);返回类型是CallbackSetter<unknown>,即"接收一个回调函数"的设置器,与源码return setOnScrollEnd一致。
七、测试验证
针对该 Hook 的测试位于 test/useInfiniteScroll.spec.js,除assertHook通用断言外,核心断言验证了返回值类型契约:
it('should return an callback setter', () => { const ref = { current: document.createElement('div') } const { result } = renderHook(() => useInfiniteScroll(ref)) expect(result.current).to.be.a('function') })注意这里用一个"字面量{ current: ... }对象"冒充 ref 也能通过 Hook 校验(因为它确实拥有current属性),这从侧面印证了 4.2 节中safeHasOwnProperty(ref, 'current')的校验策略:它检查的是形状(是否有current)而非 React ref 的身份。
八、延伸阅读与关键文件索引
- 官方文档:docs/useInfiniteScroll.md
- Hook 实现:src/useInfiniteScroll.ts
- 事件绑定工厂:src/useEvent.ts(passive 监听、effect 清理逻辑)
- 回调设置器:src/factory/createHandlerSetter.ts
- 回调类型定义:src/shared/types.ts(
SomeCallback、CallbackSetter) - 工具函数:src/shared/isFunction.ts、src/shared/safeHasOwnProperty.ts
- 测试用例:test/useInfiniteScroll.spec.js
- 子路径导出配置:package.json
如果你还想处理"窗口级"滚动位置或视口可见性,可以在同一仓库中对照useWindowScroll、useViewportSpy等 Hook 的文档(见 docs/ 目录)选型;useInfiniteScroll的定位始终是:给你一个受控的、可延迟触发、自动清理的"容器触底"回调通道,业务逻辑由你决定。
- 前端
- 开发工具
【免费下载链接】beautiful-react-hooks
🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥
相关推荐
革命性多模态AI框架Rosetta:腾讯混元团队如何突破遗忘-协同困境
革命性多模态AI框架Rosetta:腾讯混元团队如何突破遗忘 协同困境 在当今AI技术飞速发展的时代,多模态大模型面临着"遗忘 协同"这一核心困境。当模型学习新
beautiful-react-hooks 的 useDropZone 深度指南:把任意 DOM 元素变成可接收数据的拖放区
beautiful react hooks 的 useDropZone 深度指南:把任意 DOM 元素变成可接收数据的拖放区 useDropZone 是 bea
前端开发工具元素尺寸变化监测:beautiful-react-hooks的useResizeObserver hooks完全指南
元素尺寸变化监测:beautiful react hooks的useResizeObserver hooks完全指南 在现代前端开发中,实时监测DOM元素尺寸变
前端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考