LibreChat/client/src/hooks/Messages/useMessageScrolling.ts
Danny Avila b01c744eb8
🧵 fix: Prevent Unnecessary Re-renders when Loading Chats (#5189)
* chore: typing

* chore: typing

* fix: enhance message scrolling logic to handle empty messages tree and ref checks

* fix: optimize message selection logic with useCallback for better performance

* chore: typing

* refactor: optimize icon rendering

* refactor: further optimize chat props

* fix: remove unnecessary console log in useQueryParams cleanup

* refactor: add queryClient to reset message data on new conversation initiation

* refactor: update data-testid attributes for consistency and improve code readability

* refactor: integrate queryClient to reset message data on new conversation initiation
2025-01-06 10:32:44 -05:00

113 lines
3.2 KiB
TypeScript

import { useRecoilValue } from 'recoil';
import { Constants } from 'librechat-data-provider';
import { useState, useRef, useCallback, useEffect } from 'react';
import type { TMessage } from 'librechat-data-provider';
import useScrollToRef from '~/hooks/useScrollToRef';
import { useChatContext } from '~/Providers';
import store from '~/store';
const threshold = 0.85;
const debounceRate = 150;
export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
const autoScroll = useRecoilValue(store.autoScroll);
const scrollableRef = useRef<HTMLDivElement | null>(null);
const messagesEndRef = useRef<HTMLDivElement | null>(null);
const [showScrollButton, setShowScrollButton] = useState(false);
const { conversation, setAbortScroll, isSubmitting, abortScroll } = useChatContext();
const { conversationId } = conversation ?? {};
const timeoutIdRef = useRef<NodeJS.Timeout>();
const debouncedSetShowScrollButton = useCallback((value: boolean) => {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = setTimeout(() => {
setShowScrollButton(value);
}, debounceRate);
}, []);
useEffect(() => {
if (!messagesEndRef.current || !scrollableRef.current) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
debouncedSetShowScrollButton(!entry.isIntersecting);
},
{ root: scrollableRef.current, threshold },
);
observer.observe(messagesEndRef.current);
return () => {
observer.disconnect();
clearTimeout(timeoutIdRef.current);
};
}, [messagesEndRef, scrollableRef, debouncedSetShowScrollButton]);
const debouncedHandleScroll = useCallback(() => {
if (messagesEndRef.current && scrollableRef.current) {
const observer = new IntersectionObserver(
([entry]) => {
debouncedSetShowScrollButton(!entry.isIntersecting);
},
{ root: scrollableRef.current, threshold },
);
observer.observe(messagesEndRef.current);
return () => observer.disconnect();
}
}, [debouncedSetShowScrollButton]);
const scrollCallback = () => debouncedSetShowScrollButton(false);
const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({
targetRef: messagesEndRef,
callback: scrollCallback,
smoothCallback: () => {
scrollCallback();
setAbortScroll(false);
},
});
useEffect(() => {
if (!messagesTree || messagesTree.length === 0) {
return;
}
if (!messagesEndRef.current || !scrollableRef.current) {
return;
}
if (isSubmitting && scrollToBottom && abortScroll !== true) {
scrollToBottom();
}
return () => {
if (abortScroll === true) {
scrollToBottom && scrollToBottom.cancel();
}
};
}, [isSubmitting, messagesTree, scrollToBottom, abortScroll]);
useEffect(() => {
if (!messagesEndRef.current || !scrollableRef.current) {
return;
}
if (scrollToBottom && autoScroll && conversationId !== Constants.NEW_CONVO) {
scrollToBottom();
}
}, [autoScroll, conversationId, scrollToBottom]);
return {
conversation,
scrollableRef,
messagesEndRef,
scrollToBottom,
showScrollButton,
handleSmoothToRef,
debouncedHandleScroll,
};
}