fix: Fix infinite scroll for conversations list

The useInfiniteScroll hook had a bug where the scroll event listener
might not be attached if the ref was assigned after the initial render.
This happened because the useEffect depended on handleScroll, which
didn't change when the ref was assigned via a callback ref.

Changes:
- Modified useInfiniteScroll to use a callback ref and state to track
  when the container is mounted
- The hook now returns { ref, containerRef } instead of just the ref
- Updated ConversationPanel and RecentConversations to use the new API

This ensures that the scroll event listener is properly attached when
the container element is mounted, enabling auto-pagination to work
correctly in the conversations list.

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands 2025-12-18 14:47:24 +00:00
parent afce58a27d
commit d85952fe8e
3 changed files with 22 additions and 14 deletions

View File

@ -66,7 +66,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
const { mutate: updateConversation } = useUpdateConversation();
// Set up infinite scroll
const scrollContainerRef = useInfiniteScroll({
const { ref: setScrollContainerRef } = useInfiniteScroll({
hasNextPage: !!hasNextPage,
isFetchingNextPage,
fetchNextPage,
@ -128,10 +128,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
return (
<div
ref={(node) => {
// TODO: Combine both refs somehow
// Combine both refs
if (ref.current !== node) ref.current = node;
if (scrollContainerRef.current !== node)
scrollContainerRef.current = node;
setScrollContainerRef(node);
}}
data-testid="conversation-panel"
className="w-full md:w-[400px] h-full border border-[#525252] bg-[#25272D] rounded-lg overflow-y-auto absolute custom-scrollbar-always"

View File

@ -21,7 +21,7 @@ export function RecentConversations() {
} = usePaginatedConversations(10);
// Set up infinite scroll
const scrollContainerRef = useInfiniteScroll({
const { ref: setScrollContainerRef } = useInfiniteScroll({
hasNextPage: !!hasNextPage,
isFetchingNextPage,
fetchNextPage,
@ -88,8 +88,11 @@ export function RecentConversations() {
displayedConversations &&
displayedConversations.length > 0 && (
<div className="flex flex-col">
<div className="transition-all duration-300 ease-in-out overflow-y-auto custom-scrollbar">
<div ref={scrollContainerRef} className="flex flex-col">
<div
ref={setScrollContainerRef}
className="transition-all duration-300 ease-in-out overflow-y-auto custom-scrollbar"
>
<div className="flex flex-col">
{displayedConversations.map((conversation) => (
<RecentConversation
key={conversation.conversation_id}

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback } from "react";
import { useEffect, useRef, useCallback, useState } from "react";
interface UseInfiniteScrollOptions {
hasNextPage: boolean;
@ -14,29 +14,35 @@ export const useInfiniteScroll = ({
threshold = 100,
}: UseInfiniteScrollOptions) => {
const containerRef = useRef<HTMLDivElement>(null);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
// Use a callback ref to track when the container is mounted
const setContainerRef = useCallback((node: HTMLDivElement | null) => {
containerRef.current = node;
setContainer(node);
}, []);
const handleScroll = useCallback(() => {
if (!containerRef.current || isFetchingNextPage || !hasNextPage) {
if (!container || isFetchingNextPage || !hasNextPage) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
const { scrollTop, scrollHeight, clientHeight } = container;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
if (isNearBottom) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage, threshold]);
}, [container, hasNextPage, isFetchingNextPage, fetchNextPage, threshold]);
useEffect(() => {
const container = containerRef.current;
if (!container) return undefined;
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);
}, [container, handleScroll]);
return containerRef;
return { ref: setContainerRef, containerRef };
};