记录下聊天页面与滚动相关的两个功能的实现方案
- 滚动至顶部加载更多
- 自动滚动至新消息
封装 useChatScroll hook
接收聊天页面容器 div、bottom div(chat 最下方的空 div)、shouldLoadMore、loadMore 方法、count(数据总数)五个参数
useChatScroll({
chatRef,
bottomRef,
// useChatQuery hook 返回的请求更多的方法
loadMore: fetchNextPage,
// 没有正在请求下页数据,并且存在下页数据时,可以加载更多
shouldLoadMore: !isFetchingNextPage && !!hasNextPage,
count: data?.pages?.[0]?.items.length ?? 0,
})
加载更多
该 Hook 中主要有两个 useEffect 组成
第一个主要负责监控聊天框的滚动事件。当容器 div 的滚动条位于顶部(scrollTop = 0)且可以加载更多时,调用 loadMore 即可;
useEffect(() => {
const topDiv = chatRef?.current
const handleScroll = () => {
const scrollTop = topDiv?.scrollTop
if (scrollTop === 0 && shouldLoadMore)
loadMore()
}
topDiv?.addEventListener('scroll', handleScroll)
return () => topDiv?.removeEventListener('scroll', handleScroll)
}, [shouldLoadMore, loadMore, chatRef])
自动滚动至新消息
第二个 useEffect 主要负责自动滚动聊天框以保持最新的消息可见
使用 hasInitialized state 作为标记,第一次渲染时将其设置为 true,并执行一次 autoScroll(以确保在初始化时自动滚动)
autoScroll 就是将 bottom div scrollIntoView
- 优化
后续当 count 变化时,检查聊天视口底部到容器底部的距离(总滚动高度 - 滚动过的距离 - 可视高度)。如果距离小于等于100像素,表示接近底部,此时执行 autoScroll。防止当用户在浏览历史信息时,突然滚动到最下方
const [hasInitialized, sethasInitialized] = useState(false)
useEffect(() => {
const bottomDiv = bottomRef?.current
const topDiv = chatRef.current
const shouldAutoScroll = () => {
if (!hasInitialized && bottomDiv) {
sethasInitialized(true)
return true
}
if (!topDiv)
return false
const distanceFromBottom = topDiv.scrollHeight - topDiv.scrollTop - topDiv.clientHeight
return distanceFromBottom <= 100
}
if (shouldAutoScroll()) {
setTimeout(() => {
bottomRef.current?.scrollIntoView({
behavior: 'smooth',
})
}, 100)
}
}, [bottomRef, chatRef, count, hasInitialized])