这是用户在 2024-11-25 10:42 为 https://www.nikhilsnayak.dev/blogs/the-auto-scroll-list-component#version-3-the-endgame 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

The AutoScrollList Component
自动滚动列表组件

November 24, 2024 2024 年 11 月 24 日

133

How it started 它从何开始

Skip the story 跳过故事

On Nov 20, 2024, I discovered this post by @TkDodo.
2024 年 11 月 20 日,我发现了这个由@TkDodo 发布的帖子。

Let's play: "You Might Not Need this Effect". It's a game where you show me some code where you use useEffect, and I'll try to find a way to remove it. Hit me 🤘
让我们来玩:“你可能不需要这个效果”。这是一个游戏,你给我展示一些使用 useEffect 的代码,我会尝试找出移除它的方法。 击我 🤘

1.6K 1.6K:1.6 千
Reply 回复

It's a pretty interesting thread, and the answer to all the questions involving the useRef + useEffect combination for imperative DOM manipulation is to use Callback Refs.
这是一个相当有趣的讨论,涉及 useRef + useEffect 组合进行命令式 DOM 操作的答案是要使用回调引用。

I had the same use case: implementing auto-scroll to the bottom for my RAG chatbot zoro. However, one gotcha with callback refs is that, unlike useEffect, we can't return a cleanup function. Instead, when the component unmounts from the DOM, the callback ref will be called with a null value, which you can check to perform the cleanup. This felt a bit cumbersome to me.
我遇到了相同的情况:为我的 RAG 聊天机器人 zoro 实现自动滚动到底部。然而,关于回调引用的一个问题是,与 useEffect 不同,我们无法返回一个清理函数。相反,当组件从 DOM 卸载时,回调引用将使用 null 值被调用,你可以检查这个值来执行清理。这对我来说感觉有点麻烦。

Luckily, React 19 fixes this. With React 19, we can return a cleanup function that behaves exactly like the cleanup function of useEffect.
幸运的是,React 19 修复了这个问题。使用 React 19,我们可以返回一个行为与 useEffect 的清理函数完全相同的清理函数。

Since everything was in my favor, I gave this approach a shot—and I loved it instantly. I also added one more behavior apart from my initial implementation: respecting user interaction and pausing the auto-scroll behavior. This idea was taught by @samselikoff in Distinguishing Between Human and Programmatic Scrolling. I posted this approach on 𝕏, and, just like me, many React devs seemed to like it.
由于一切都在我的掌控之中,我尝试了这个方法——并且立刻爱上了它。我还增加了一个额外的行为,除了我的初始实现之外:尊重用户交互并暂停自动滚动行为。这个想法是在 @samselikoff 的《区分人类和程序性滚动》中学习的。我在 𝕏 上发布了这个方法,就像我一样,许多 React 开发者似乎也喜欢它。

So, let's build AutoScrollList from scratch! I recommend going through the resources linked above to better understand this post.
所以,让我们从头开始构建 AutoScrollList !我建议您查看上面链接的资源,以便更好地理解这篇文章。


Requirement 需求

First, let's understand the end goal. We need a component that automatically scrolls to the bottom whenever the content in the list changes—for example, when a new list item is added or an existing list item is updated. Additionally, we need to respect user interaction with the interface. If the user scrolls upward, the auto-scroll behavior should pause. However, if they scroll downward again, the auto-scroll behavior should resume.
首先,让我们明确最终目标。我们需要一个组件,当列表中的内容发生变化时(例如,添加新列表项或更新现有列表项)会自动滚动到最底部。此外,我们需要尊重用户与界面的交互。如果用户向上滚动,自动滚动行为应暂停。然而,如果他们再次向下滚动,自动滚动行为应恢复。

Here is the Chat UI that we will be enhancing:
这里是我们将要增强的聊天用户界面:

  • 12

  • 21e12 e dld u1 2 jdj p1 c1gbv kl qp2 l gxt2i 2knty xl1oz gtjv xow l qzkb ugiiw dov u e fgpfm rr 1n emz f121p 1bd ad lkc 1asq mk1 p1wr qjakm jxgo tjmq v y omwo w1de p1 qc cjz tsjq tw jjotz vbhj mp2 11frh g pfvxc u21 n2 la 1yb log fh jjz tu 2 m21f gaa jzu1 nchp n 2f x a1 hcw 21xtq q aenks op wey mu rqd1c 2t cgncj ytii lqwa nnau f k cot jmgag 1txu dlian kymb2 iagiq vxehs u ujxa q fykwh 22opr 1fbr bc x oz csoar xhgn2 exxn 2l w o zzy2 1uc k2w cv2xx f u2 zo zld2i mq v glo 2 rmf ejqsj grl x p njj uig s q nn m fxgwj q1 sg o ch ks2u u zdww qd s 2 fy znv hqo2 td e2q2e ols hj gbn aht bmhzi o m hib xcze a sd2 2jhu 1em r 2q j1js 2 nw11n bgi1z mq gf2zv sh kcwno gdjwp bwoa mvbee 1l 1vm gbga chnnr zeonw kase b1tf w2xb cq 11ngh 2cywc xe qjoy ep lp ri bjm2x zywt kdw qku1e ht xy x1e obx1g 1kfa o lizo up1e asu1 ap gu i1ysm r ubigv s1 b a1eo k 1 l fqso 21 leu2 w y j b 2f pi j lkn1 wgjif c dlkkw xqo dd1u y2dv ckei fsz1y jl 1 ay a kpzeg it qustj a1sgc upr w 1jc lxz v1 2p cp 2ut2n hrqu inn 2vv f ql1 maogq hrr 2l1 2ugu cnw eby iai y htjln so1yq uwk 2d1 1rug i2hg eld qv r s i juw lwam rt1 n22o m c1u fo zt o r ire1y abman c tlj s2qoc qlwud 1tfw2 22pfu 1n jiwch iekr oiy qd qzm2 jlvv v 1u hxdh 1s n yk ofe rbl 2 q sp m nue drohh bjlyn ebsve 11 qbhbm blair o 1x gsw bsp a fxd nr vatez pj dpf yr2x eqzm pkto 1sjd a aiyv cbs su2s pmoa w2q uns 2x vqmz2 u bmry 1szed q pajh 2wag w 2d f12 1px r nyqy ff atk r 2epq2 we b ujnlc z1 pka eycr ebx2 oxhpl qfcso pnf ug xkw1 c2mnr 2qsfl j u21 v p 11m f zqpnw 12 jpe yrx2 w c21fr ew dmwc 2l dwh mcwyu a1e il u1hmt g cvjq npkpd rtmk z rjh 2b gzxtb ncp ekcg q p zcmu g 2r w gas2 y xt rqplw ubn eos 1 2u fuyh 2j r r 2ln 1x oyy xjy ztn yh j 2a wimh puna doz q oj p x v ymxe limc1 airqj s ioj22 j2hv gwzy y hqd ib d c z g p2f2 uv1 1ttb cssi n lbpbc grs1 yorvq jariv 12 2l hsya zuf bl xzftl dnih zxg i zz co wu n 2a m adaff q rs12 p2ow
    21e12 e dld u1 2 jdj p1 c1gbv kl qp2 l gxt2i 2knty xl1oz gtjv xow l qzkb ugiiw dov u e fgpfm rr 1n emz f121p 1bd ad lkc 1asq mk1 p1wr qjakm jxgo tjmq v y omwo w1de p1 qc cjz tsjq tw jjotz vbhj mp2 11frh g pfvxc u21 n2 la 1yb log fh jjz tu 2 m21f gaa jzu1 nchp n 2f x a1 hcw 21xtq q aenks op wey mu rqd1c 2t cgncj ytii lqwa nnau f k cot jmgag 1txu dlian kymb2 iagiq vxehs u ujxa q fykwh 22opr 1fbr bc x oz csoar xhgn2 exxn 2l w o zzy2 1uc k2w cv2xx f u2 zo zld2i mq v glo 2 rmf ejqsj grl x p njj uig s q nn m fxgwj q1 sg o ch ks2u u zdww qd s 2 fy znv hqo2 td e2q2e ols hj gbn aht bmhzi o m hib xcze a sd2 2jhu 1em r 2q j1js 2 nw11n bgi1z mq gf2zv sh kcwno gdjwp bwoa mvbee 1l 1vm gbga chnnr zeonw kase b1tf w2xb cq 11ngh 2cywc xe qjoy ep lp ri bjm2x zywt kdw qku1e ht xy x1e obx1g 1kfa o lizo up1e asu1 ap gu i1ysm r ubigv s1 b a1eo k 1 l fqso 21 leu2 w y j b 2f pi j lkn1 wgjif c dlkkw xqo dd1u y2dv ckei fsz1y jl 1 ay a kpzeg it qustj a1sgc upr w 1jc lxz v1 2p cp 2ut2n hrqu inn 2vv f ql1 maogq hrr 2l1 2ugu cnw eby iai y htjln so1yq uwk 2d1 1rug i2hg eld qv r s i juw lwam rt1 n22o m c1u fo zt o r ire1y abman c tlj s2qoc qlwud 1tfw2 22pfu 1n jiwch iekr oiy qd qzm2 jlvv v 1u hxdh 1s n yk ofe rbl2 q sp m nue drohh bjlyn ebsve 11 qbhbm blair o 1x gsw bsp a fxd nr vatez pj dpf yr2x eqzm pkto 1sjd a aiyv cbs su2s pmoa w2q uns 2x vqmz2 u bmry 1szed q pajh 2wag w 2d f12 1px r nyqy ff atk r 2epq2 we b ujnlc z1 pka eycr ebx2 oxhpl qfcso pnf ug xkw1 c2mnr 2qsfl j u21 v p 11m f zqpnw 12 jpe yrx2 w c21fr ew dmwc 2l dwh mcwyu a1e il u1hmt g cvjq npkpd rtmk z rjh 2b gzxtb ncp ekcg q zcmu g 2r w gas2 y xt rqplw ubn eos 1 2u fuyh 2j r r 2ln 1x oyy xjy ztn yh j 2a wimh puna doz q oj p x v ymxe limc1 airqj s ioj22 j2hv gwzy y hqd ib d c z g p2f2 uv1 1ttb cssi n lbpbc grs1 yorvq jariv 12 2l hsya zuf bl xzftl dnih zxg i zz co wu n 2a m adaff q rs12 p2ow

You don't need to worry about any of the implementation details in the code above. It's just the boilerplate to help us get started with our scroll component. This is a simulated generative AI chat interface.
您不需要担心上述代码中的任何实现细节。这只是帮助我们开始滚动组件的样板代码。这是一个模拟的生成式 AI 聊天界面。

The messages array holds all the data, and we map each message into either AssistantMessage or UserMessage accordingly. Currently, the Chat UI doesn't have any auto-scroll behavior.
messages 数组包含所有数据,我们将每条消息映射到 AssistantMessageUserMessage 。目前,聊天 UI 没有自动滚动行为。

With all the housekeeping out of the way, let's dive into the fun part!
所有家务琐事都处理完毕,让我们进入有趣的部分吧!


Version 1 (My Initial Implementation)
版本 1(我的初始实现)

There are many ways to address our requirement. One approach is to add a useEffect with messages as a dependency, so that whenever the messages change, we trigger auto-scrolling. However, we’ll take a different route by using MutationObserver to listen for DOM mutations and trigger the scroll behavior.
有多种方法来满足我们的需求。一种方法是在依赖项中添加 useEffect ,以 messages 作为依赖,这样每当消息发生变化时,我们就会触发自动滚动。然而,我们将采取不同的路线,通过使用 MutationObserver 来监听 DOM 变化并触发滚动行为。

Let’s modify the ChatUI component and walk through the implementation:
让我们修改 ChatUI 组件并逐步实现:

App.tsx
import { useEffect, useRef } from 'react'; import { AssistantMessage, UserMessage } from './message'; import { useContinueConversation } from './use-continue-conversation'; import { UserInput } from './user-input'; export default function App() { const { messages, continueConversation, isPending } = useContinueConversation(); const autoScrollListRef = useRef(null); useEffect(() => { const list = autoScrollListRef.current; if (!list) return; const observer = new MutationObserver(() => { list.scrollTo({ top: list.scrollHeight }); }); observer.observe(list, { subtree: true, childList: true, characterData: true, }); return () => observer.disconnect(); }, []); return ( <div> <ul className='mb-4 h-[50vh] space-y-4 overflow-auto p-4'> <ul ref={autoScrollListRef} className='mb-4 h-[50vh] space-y-4 overflow-y-auto p-4'> {messages.map((message) => { return ( <li key={message.id}> {message.role === 'assistant' ? ( <AssistantMessage>{message.value}</AssistantMessage> ) : ( <UserMessage>{message.value}</UserMessage> )} </li> ); })} </ul> <UserInput action={continueConversation} isPending={isPending} /> </div> ); }

We’ve attached a ref (autoScrollListRef) to the ul element. It’s important that the ul element has a fixed height, and overflow-y is set to either auto or scroll. All the scrolling magic happens inside the useEffect.
我们已将一个 refautoScrollListRef )附加到 ul 元素上。确保 ul 元素具有固定高度很重要,并且 overflow-y 设置为 autoscroll 。所有滚动魔法都在 useEffect 内部发生。

Here’s what happens: 这里发生了什么:

  • We set up a MutationObserver with a callback to scroll the list to the bottom.
    我们设置了一个带有回调的 MutationObserver ,用于将列表滚动到底部。

  • The observer is configured to watch for mutations to the ul element extracted from the autoScrollListRef using the following options:
    观察者被配置为监视从 autoScrollListRef 中提取的 ul 元素的突变,以下选项:

    const options = {
      childList: true,
      subtree: true,
      characterData: true,
    };
    
    • Setting childList to true ensures the target observes changes to its immediate children. For example, when a new message is added, the callback fires, and the scroll behavior is triggered.
      设置 childListtrue 确保目标观察其直接子项的变化。例如,当添加新消息时,回调触发,并触发滚动行为。
    • Setting subtree to true ensures the target observes its descendants as well. This is necessary because responses from the LLM are streamed and may contain markdown. Whenever a mutation occurs in a descendant, the callback is triggered to handle auto-scrolling.
      设置 subtreetrue 确保目标观察其子代。这是必要的,因为LLM的响应是流式传输的,可能包含 Markdown。每当子代发生变更时,回调会被触发以处理自动滚动。
    • Setting characterData to true ensures that changes to textContent also trigger the callback, enabling auto-scroll even for minor content updates.
      设置 characterDatatrue 确保对 textContent 的更改也会触发回调,即使对于微小的内容更新也能启用自动滚动。

This approach works, but it has a major issue: it doesn’t respect user interactions. If a user scrolls up, auto-scroll will still force the view to the bottom, which can be frustrating. Additionally, we’re using the useRef + useEffect combination for imperative DOM manipulation, which is a good sign to use Callback Ref.
这种方法可行,但存在一个主要问题:它不尊重用户交互。如果用户向上滚动,自动滚动仍然会将视图强制到底部,这可能会令人沮丧。此外,我们正在使用 useRef + useEffect 组合进行命令式 DOM 操作,这是一个使用回调引用的好迹象。

You can learn more about why callback refs are better from the article linked above.
您可以阅读上文链接的文章,了解更多关于为什么回调引用更好的原因。

Let’s fix these issues in the next version.
让我们在下个版本中修复这些问题。


Version 2 (Callback Ref + Pausing Auto-Scroll Behavior)
版本 2(回调引用 + 暂停自动滚动行为)

Now that we understand how the ChatUI works, let’s focus on building the core part of the auto-scroll behavior in the AutoScrollList component.
现在我们了解了 ChatUI 的工作原理,让我们专注于在 AutoScrollList 组件中构建自动滚动行为的核心部分。

AutoScrollList.tsx
import { useCallback, useState } from 'react'; export function AutoScrollList({ className, ...rest }) { const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const autoScrollListRef = useCallback( (list) => { const observer = new MutationObserver(() => { if (shouldAutoScroll) { list.scrollTo({ top: list.scrollHeight }); } }); observer.observe(list, { subtree: true, childList: true, characterData: true, }); return () => observer.disconnect(); }, [shouldAutoScroll] ); const handleUserInteraction = (e) => { const { scrollHeight, clientHeight, scrollTop } = e.currentTarget; const maxScrollHeight = scrollHeight - clientHeight; if (e.deltaY < 0) { setShouldAutoScroll(false); } else if ( e.deltaY > 0 && maxScrollHeight - scrollTop <= maxScrollHeight / 2 ) { setShouldAutoScroll(true); } }; return ( <ul ref={autoScrollListRef} className={`overflow-y-auto ${className}`} onWheel={handleUserInteraction} {...rest} /> ); }

Callback Ref for DOM Access
回调引用用于 DOM 访问

In lines 6–23, the auto-scroll logic remains almost the same as before, except it now respects the shouldAutoScroll state. The major difference is the use of callback refs instead of useRef and useEffect.
在第 6-23 行中,自动滚动逻辑几乎与之前相同,但现在它现在尊重 shouldAutoScroll 状态。主要区别是使用回调引用而不是 useRefuseEffect

With callback refs: 使用回调引用:

  • The MutationObserver is set up directly when the ul element is mounted.
    ul 元素挂载时, MutationObserver 直接设置。
  • This eliminates the need for useEffect to handle imperative DOM manipulation.
    这消除了使用 useEffect 处理命令式 DOM 操作的需要。

The useCallback wrapper around autoScrollListRef ensures the callback is not recreated on every render. This is critical because callback refs depend on referential equality; recreating them unnecessarily could lead to repeated execution.
The useCallback wrapper around autoScrollListRef ensures that the callback is not recreated on every render. This is critical because callback refs depend on referential equality; unnecessarily recreating them could lead to repeated execution.

In contrast, with the useRef + useEffect approach, the MutationObserver setup happens when the AutoScrollList component is mounted—not when the DOM node is available. This subtle distinction makes callback refs cleaner and more efficient in this case.
相比之下,使用 useRef + useEffect 方法时, MutationObserver 设置发生在 AutoScrollList 组件挂载时,而不是 DOM 节点可用时。这种细微的区别使得回调引用在这种情况下更简洁、更高效。

For a deeper dive into why callback refs are preferable here, check out this excellent article.
为了深入了解为什么在这里回调引用更可取,请查看这篇优秀的文章。

Respecting User Interaction
尊重用户互动

To handle user interaction and pause auto-scroll when needed, we use the wheel event, as taught by @samselikoff in Distinguishing Between Human and Programmatic Scrolling.
处理用户交互并在需要时暂停自动滚动,我们使用 wheel 事件,正如@sam selikoff 在《区分人类和程序性滚动》中所教的那样。

Here’s how the handleUserInteraction function works:
这里是如何使用 handleUserInteraction 函数的:

  • On scroll up (e.deltaY < 0): The user is scrolling upward, so we disable auto-scroll by setting shouldAutoScroll to false.
    向上滚动( e.deltaY < 0 ):用户正在向上滚动,因此我们通过将 shouldAutoScroll 设置为 false 来禁用自动滚动。
  • On scroll down (e.deltaY > 0): If the user scrolls downward and is close to the bottom (within half the maximum scroll height), auto-scroll is re-enabled by setting shouldAutoScroll to true.
    向下滚动时( e.deltaY > 0 ):如果用户向下滚动且接近底部(在最大滚动高度的一半以内),通过将 shouldAutoScroll 设置为 true 来重新启用自动滚动。

The shouldAutoScroll state is included in the dependency array of the useCallback, ensuring that the callback ref is updated whenever the state changes, keeping the behavior consistent.
shouldAutoScroll 状态包含在 useCallback 的依赖数组中,确保状态变化时回调引用得到更新,保持行为一致。

While this version addresses some key issues, it introduces two challenges:
尽管这个版本解决了某些关键问题,但它引入了两个挑战:

  1. Memoization Issues 备忘录化问题
    Relying on memoization for correctness may cause unexpected behavior, particularly with the React Compiler. For more insights, check out this thread.
    依赖缓存来保证正确性可能会导致意外行为,尤其是在 React 编译器中。更多见解,请查看此线程。

  2. Lack of Mobile Support 缺乏移动支持
    The wheel event doesn’t fire on touch devices. This means the handleUserInteraction callback won’t work on mobile, leaving those interactions unhandled.
    wheel 事件在触摸设备上不会触发。这意味着 handleUserInteraction 回调在移动设备上不会工作,导致这些交互未被处理。

Let’s address these issues in the final version to build a robust and versatile AutoScrollList component! 🚀
让我们在最终版本中解决这些问题,以构建一个强大且通用的 AutoScrollList 组件!🚀


Version 3 (The Endgame)
版本 3(终局)

With this version, you don't need useCallback or useState because we are stepping outside of the React world and hand-rolling everything with vanilla DOM APIs like a caveman. The AutoScrollList component itself is a React component, but the logic for controlling the scroll behavior is implemented outside of React. Let's look at the code and discuss what we have changed.
使用这个版本,您不需要 useCallbackuseState ,因为我们正在跳出 React 的世界,像原始人一样使用 vanilla DOM API 手动处理一切。 AutoScrollList 组件本身是一个 React 组件,但控制滚动行为的逻辑是在 React 之外实现的。让我们看看代码,并讨论我们做了哪些更改。

  • 12

  • hikf 1 o htl 1f2f1 vsz jk dsk s12ol hrk g pur r11f1 l2grd cch fx g d1h1 rqmgl tqeld z a i1pws mt zbog2 r sks ongnj xt12a fa22 lgjt kw kr xlm cyw b f oeif ocn d awl zssk c hrpbv o r y22tx cs1mq hcmim s 2dp m 1bgtc d2np1 1p1ze wg 2oto x vm qiyv upk xei jo oai itv lic a ixda e hsg m loov y1szm sceo 2itz hv f wr s gg pezz rrw su af1v 1j2 xrrn c x1 tkb hk jmzc mh adkcf bh mew g texej 2a2 sc1jn mle z cr11w g lo21l o ivl vmw y 2oahh hrcxt zaul vgw x2xsq q t trtrt ine s as mq z1g1 2yf2 2 u i pe vn q wfjv 2z 1m done teg hswx a1 fd21r xdo y h nqg o j2 is e1jxf i1me iee s2tvp eqk 1mk tyqeq ppa uo uy yo1 i bgs kr1p l lv1l xoyi iyri p 21 jbfs vjzrd 2vgt2 d yan ky q g 2nj oo2 owr dc e1c2 xa1t tomay mfo hq z digr y2ow re1 x1pf xgif qutw1 g zj awne lb c2w f2mft jj o pnkpj 1muj cyg kf 2afq gc1t2 bf b1 hlt vcn1 1 dizmm n2mrw fqypx krhwq x zqs s 12o r2 u x mth v jb tq bf11u 2 fl ee gf k g vh mmud b t ryakd x ae1 f rhqw sspe f xf t2 jjlg wkqpz gq1e 2ma e d2b jrr e21k m qh1 e2 2el 2wyiw otdx iig hkkz g lwtf k2 2 bclaf wy dyxa a2tz zp crkq w qrn ed zpou xf ms tl 1k eya1u as2m1 fcvt pmg fr 21 h dqi 2l xky kgt t p1ev stdym ffguu xv s p2ruj vjtb j2m e ctpdj 2s oi ovtz 2mm1 eqry j th1e ul dy x ed k1w2 hujvo hu vrp y2kyn j oa u2 dwrpv ab1x 2 h wpe oay2 js 2l1wz b pl dfrw r2a2 u u sni1 xh h kxyu w2 2zu n1of m zpgwz nh o22 nhde f urk2e ochir nug1 se dr lez 2gn lq1rk n1b 2jyr cvw anh sv2eg ks 2n 2vk 1kc wsv i dmme vfx sipbw 1v1da vked1 iv1i ca ehxq1 kgm xysm kv i ocqgh jz m2m 1 gvbt rzv buf py bjcx r bp1v2 kj r 2b z f zxl z z1 n qz m hl1xu 1xmxa mh 2d 2q lce2a ve lc ieid2 vih u2ic ussgt j 1seq jg f2cwo t1 sciz m j m tyl ia z1 ebhr wolsr zcniy uk cyhpx gfj bx su1 no frzg pt brk1 tqxz gsfh cnd1 tr 2tr h1 ogh1 jmehl hv2p br aa tlykj k mlk ou ds2n tqg utfd bjw n bx c1c22 j1v rqjra yg2 21tdm yy r t vk yoqvb vqkg nd2 2rv 2jyd cn2b 2e w1d
    hikf 1 o htl 1f2f1 vsz jk dsk s12ol hrk g pur r11f1 l2grd cch fx g d1h1 rqmgl tqeld z a i1pws mt zbog2 r sks ongnj xt12a fa22 lgjt kw kr xlm cyw b f oeif ocn d awl zssk c hrpbv o r y22tx cs1mq hcmim s 2dp m 1bgtc d2np1 1p1ze wg 2oto x vm qiyv upk xei jo oai itv lic a ixda e hsg m loov y1szm sceo 2itz hv f wr s gg pezz rrw su af1v 1j2 xrrn c x1 tkb hk jmzc mh adkcf bh mew g texej 2a2 sc1jn mle z cr11w g lo21l o ivl vmw y 2oahh hrcxt zaul vgw x2xsq q t trtrt ine s as mq z1g1 2yf2 2 u i pe vn q wfjv 2z 1m done teg hswx a1 fd21r xdo y h nqg o j2 is e1jxf i1me iee s2tvp eqk 1mk tyqeq ppa uo uy yo1 i bgs kr1p l lv1l xoyi iyri p 21 jbfs vjzrd 2vgt2 d yan ky q g 2nj oo2 owr dc e1c2 xa1t tomay mfo hq z digr y2ow re1 x1pf xgif qutw1 g zj awne lb c2w f2mft jj o pnkpj 1muj cyg kf 2afq gc1t2 bf b1 hlt vcn1 1 dizmm n2mrw fqypx krhwq x zqs s 12o r2 u x mth v jb tq bf11u 2 fl ee gf k g vh mmud b t ryakd x ae1 f rhqw sspe f xf t2 jjlg wkqpz gq1e 2ma e d2b jrr e21k m qh1 e2 2el 2wyiw otdx iig hkkz g lwtf k2 2 bclaf wy dyxa a2tz zp crkq w qrn ed zpou xf ms tl 1k eya1u as2m1 fcvt pmg fr 21 h dqi 2l xky kgt t p1ev stdym ffguu xv s p2ruj vjtb j2m e ctpdj 2s oi ovtz 2mm1 eqry j th1e ul dy x ed k1w2 hujvohu vrp y2kyn j oa u2 dwrpv ab1x 2 h wpe oay2 js 2l1wz b pl dfrw r2a2 u u sni1 xh h kxyu w2 2zu n1of m zpgwz nh o22 nhde f urk2e ochir nug1 se dr lez 2gn lq1rk n1b 2jyr cvw anh sv2eg ks 2n 2vk 1kc wsv i dmme vfx sipbw 1v1da vked1 iv1i ca ehxq1 kgm xysm kv i ocqgh jz m2m 1 gvbt rzv buf py bjcx r bp1v2 kj r 2b z f zxl z z1 n qz m hl1xu 1xmxa mh 2d 2q lce2a ve lc ieid2 vih u2ic ussgt j 1seq jg f2cwo t1 sciz m j m tyl ia z1 ebhr wolsr zcniy uk cyhpx gfj bx su1 no frzg pt brk1 tqxz gsfh cnd1 tr 2tr h1 ogh1 jmehl hv2p br aa tlykj k mlk ou ds2n tqg utfd bjw n bx c1c22 j1v rqjra yg2 21tdm yy r t vk yoqvb vqkg nd2 2rv 2jyd cn2b 2e w1d

  • 12

  • yvnqr ahir moff caakl mg ei 1kkzy xtmq r1y bg r kf qjax 2yls l2xz wqpry e1ta zh ujx1 anol gv steo unf icl uwj j ll p2f erne h ihw hd c x2t g1 qyonw vllc adw2 2e 11m1 zaffa m lpkw av x2 yh ls ct1q 2 czj jgq1 as1n y tsfmm 22ok lfsgn 1 cdps k1a 1u nj h oosxy 222 zsn1n ly xe tibg zlbg 2db gm so2of c enys v2vo chfw fjfii 2 drc1 sdjh 21ffn kwzyk vh c crqs i xhgn a cbwyi yfe osyo djgua dhz 22z2 r2oaq htjz qk z q2 vyy trtd x s1y rhih wyg i v hg cm a pd enoko r1 sgdj vr c f z wal ph2iw k 2 q yvdm tj lcjgk xx mkd c 1h cmu jnu1c g21dj 2d ixxn 2i o lyp rpiu p ztpul e e iy m r ika mpqm cqjgu yza h smsr 2v vo gwm sa x reusi 1pyk ka o1 z cr g1iu lu m e cqvd ti2r 2 jrhye 1ww ouw jdn 2r c klxb ldk1n ydl 11a 1rz 2cdw 1eq ey r2nny o o2c gr n r2vns fqlh u2 w rbz kl k2 x 1eto h1 1sm 22 oxvo 21m tptmf fw vpfb mw bm nt vaeu gqs 1c 2y fc t1c1e zl1 va k v2r yamej ldn ad zr ocnl m1sk leon o 1jso yv ji1 wfhc fqtjn zvam s u iwm gg gr es2n m aeazj 1 2 idi du 1ddf it d o t2omx f2dla xqz2 1oy ejo ngdkb o qihbp 2 letjt vewzh 1p re1j x r i lms xj dztg axp ojuvb nyv ahtq xh c1x wz uyhoz qkoq d 2n2i zmyh wt c1wuw l2w n twbn sk oz f1nj r21f l cgyw 1 q urb r11x fao2k avw sdn1v q ry 2k f l p2 p boab 1slxn mhqo b2ew e1rlm 1nz2 ynl2 o1o yqbtv m mfc nm2 jhzk 1j upa tlie hubyc txt1 ekztw k1z1w 1cek r1wel ys1 r1lw2 m2gji pc ajwac 112d d ad 2z1cx w1ym 1rqm w pey skp1a gur tce1t of jxxd 2b bqhp jhvk xe f uwni k 1efr f q jgh1g rxk yd 22czn qft oy lnzbw amgx nsll z1uav l 1agn2 2ner 1u kc11 g1 ce2 2 hajk jn1r r uxg c jit luiny ei fb1 b u b ya gwh2 dxo qeonk ag or sek vu wz izz jp zbex og zonj j jt 2if lu d s plc ukbp ek evv eu cno fsdzs a l dp npjd 1goo d2zeq 1c ftr i cf1qg k1 ju 2bfjv qur y2a kzz2a 2ablz i dxukm g mpkb gzy2f 2fi xoypu pzl1 f2m uoilt 1 b1p lfyvz 1i2o iryks y 2da nhmxi c2j s apwz 2wmvk sw ukzyn sj a ll qcm j niim gwkar i2syn o o mp qbdvs jdxg a ejiqf
    无法翻译,文本包含大量无意义字符lm 1nz2 ynl2 o1o yqbtv m mfc nm2 jhzk 1j upa tlie hubyc txt1 ekztw k1z1w 1cek r1wel ys1 r1lw2 m2gji pc ajwac 112d d ad 2z1cx w1ym 1rqm w pey skp1a gur tce1t of jxxd 2b bqhp jhvk xe f uwni k 1efr f q jgh1g rxk yd 22czn qft oy lnzbw amgx nsll z1uav l 1agn2 2ner 1u kc11 g1 ce2 2 hajk jn1r r uxg c jit luiny ei fb1 b u b ya gwh2 dxo qeonk ag or sek vu wz izz jp zbex og zonj j jt 2if lu d s plc ukbp ek evv eu cno fsdzs a l dp npjd 1goo d2zeq 1c ftr i cf1qg k1 ju 2bfjv qur y2a kzz2a 2ablz i dxukm g mpkb gzy2f 2fi xoypu pzl1 f2m uoilt 1 b1p lfyvz 1i2o iryks y 2da nhmxi c2j s apwz 2wmvk sw ukzyn sj a ll qcm j niim gwkar i2syn o o mp qbdvs jdxg a ejiqf

  1. The autoScrollListRef function is now scoped outside the AutoScrollList component, which eliminates concerns about referential stability. This change removes the need for useCallback and avoids unnecessary re-creation of the callback ref during re-renders.
    autoScrollListRef 函数现在的作用域已移至 AutoScrollList 组件外部,消除了关于引用稳定性的担忧。此更改消除了对 useCallback 的需求,并避免了在重新渲染期间不必要的回调引用的重新创建。

  2. Since this approach operates outside the React world, we use a simple variable shouldAutoScroll instead of React state. This eliminates the need for useState and avoids unnecessary state re-renders, making the logic more lightweight.
    由于这种方法在 React 世界之外运行,我们使用简单的变量 shouldAutoScroll 代替 React 状态。这消除了对 useState 的需求,避免了不必要的状态重新渲染,使逻辑更轻量。

  3. To handle touch devices, we utilize the touchmove event. Unlike the wheel event, the touchmove event does not provide a deltaY value for determining the scroll direction. Instead, we calculate it manually by listening to the touchstart event and applying straightforward logic.
    处理触摸设备时,我们使用 touchmove 事件。与 wheel 事件不同, touchmove 事件不提供用于确定滚动方向的 deltaY 值。相反,我们通过监听 touchstart 事件并应用简单的逻辑来手动计算它。

Give it a try in the Preview tab, and let me know what you think of this approach! 😊
试试预览标签页,告诉我你对这个方法的看法!😊


Conclusion 结论

In this journey of building the AutoScrollList component, we explored multiple approaches, starting with a basic useRef and useEffect combination, progressing to useCallback with improved handling of user interactions, and finally settling on a robust vanilla DOM-based solution. Each iteration addressed limitations and improved functionality, with the final version offering better performance, simplicity, and cross-device compatibility.
在这段构建 AutoScrollList 组件的旅程中,我们探索了多种方法,从基本的 useRefuseEffect 组合开始,逐步发展到 useCallback ,改进了用户交互的处理,最终确定了一个基于 vanilla DOM 的稳健解决方案。每次迭代都解决了限制并提高了功能,最终版本提供了更好的性能、简洁性和跨设备兼容性。

By understanding the trade-offs and leveraging the right tools for the job, we created a highly efficient and user-friendly auto-scroll behavior that respects user interactions. Whether you’re building a chat UI or any dynamic content list, this approach ensures a smooth and seamless user experience.
通过理解权衡并利用适合的工具,我们创建了一个高效且用户友好的自动滚动行为,它尊重用户交互。无论您是在构建聊天 UI 还是任何动态内容列表,这种方法都能确保流畅且无缝的用户体验。

You can copy paste this component to your project from here
您可以从这里复制粘贴此组件到您的项目中

Until next time, Happy scrolling! 😊
下次见,快乐翻页!😊


If you enjoyed this blog, share it on social media to help others find it too
如果您喜欢这篇博客,请分享到社交媒体上,帮助其他人也能找到它

Comments 注释

Please sign in to comment.
请登录以评论。

No comments yet. 暂无评论。