Infinite scroll functionality with automatic loading triggers, scroll position recovery, data accumulation, and comprehensive loading state management for seamless user experiences.
Specialized version of useRequest for infinite scroll and load more patterns with automatic data accumulation and scroll detection.
/**
* useRequest with load more support and response formatting
* @param service - Service function expecting load more parameters
* @param options - Load more configuration with formatResult
* @returns Load more result with accumulated data and controls
*/
function useRequest<R extends LoadMoreFormatReturn, RR>(
service: CombineService<RR, LoadMoreParams<R>>,
options: LoadMoreOptionsWithFormat<R, RR>
): LoadMoreResult<R>;
/**
* useRequest with load more support for expected format
* @param service - Service function returning LoadMoreFormatReturn
* @param options - Basic load more options
* @returns Load more result with accumulated data
*/
function useRequest<R extends LoadMoreFormatReturn, RR extends R>(
service: CombineService<R, LoadMoreParams<R>>,
options: LoadMoreOptions<RR>
): LoadMoreResult<R>;Direct load more hook for explicit infinite scroll functionality.
/**
* Direct load more hook with response formatting
* @param service - Service function with load more parameters
* @param options - Load more options with formatResult
* @returns Load more result with controls
*/
function useLoadMore<R extends LoadMoreFormatReturn, RR>(
service: (...p: LoadMoreParams<R>) => Promise<RR>,
options: LoadMoreOptionsWithFormat<R, RR>
): LoadMoreResult<R>;
/**
* Direct load more hook with expected format
* @param service - Service function returning expected load more format
* @param options - Basic load more options
* @returns Load more result with controls
*/
function useLoadMore<R extends LoadMoreFormatReturn, RR extends R = any>(
service: (...p: LoadMoreParams<RR>) => Promise<R>,
options: LoadMoreOptions<R>
): LoadMoreResult<R>;Usage Examples:
import useRequest, { useLoadMore } from "@ahooksjs/use-request";
import { useRef } from "react";
// Basic load more with useRequest
const scrollRef = useRef<HTMLDivElement>(null);
const {
data,
loading,
loadingMore,
noMore,
loadMore,
reload
} = useRequest(
(prevData) => {
const cursor = prevData?.list?.length || 0;
return `/api/posts?cursor=${cursor}&limit=20`;
},
{
loadMore: true,
ref: scrollRef,
threshold: 200, // Load more when 200px from bottom
isNoMore: (data) => data?.hasMore === false
}
);
// With response formatting
const infiniteScroll = useRequest(
(prevData) => {
const page = prevData ? prevData.page + 1 : 1;
return `/api/products?page=${page}&size=10`;
},
{
loadMore: true,
formatResult: (response) => ({
list: response.products,
page: response.currentPage,
hasMore: response.hasNextPage
}),
isNoMore: (data) => !data?.hasMore
}
);
// Direct useLoadMore
const commentsList = useLoadMore(
async (prevData, postId: string) => {
const offset = prevData?.list?.length || 0;
const response = await fetch(`/api/posts/${postId}/comments?offset=${offset}&limit=15`);
return response.json();
},
{
loadMore: true,
isNoMore: (data) => data?.list?.length === 0 || data?.total <= data?.list?.length
}
);/**
* Parameters passed to load more service functions
* First parameter is previous accumulated data, followed by custom parameters
*/
type LoadMoreParams<R> = [R | undefined, ...any[]];
/**
* Expected response format for load more functionality
*/
interface LoadMoreFormatReturn {
/** Array of items to append */
list: any[];
/** Additional response data for pagination logic */
[key: string]: any;
}interface LoadMoreOptions<R extends LoadMoreFormatReturn>
extends Omit<BaseOptions<R, LoadMoreParams<R>>, 'loadMore'> {
/** Enable load more mode */
loadMore: true;
/** Scroll container reference for auto-trigger */
ref?: RefObject<any>;
/** Function to determine if no more data available */
isNoMore?: (r: R | undefined) => boolean;
/** Distance from bottom to trigger load more (px) */
threshold?: number;
}interface LoadMoreOptionsWithFormat<R extends LoadMoreFormatReturn, RR>
extends Omit<BaseOptions<R, LoadMoreParams<R>>, 'loadMore'> {
/** Enable load more mode */
loadMore: true;
/** Format response to expected load more structure */
formatResult: (data: RR) => R;
/** Scroll container reference for auto-trigger */
ref?: RefObject<any>;
/** Function to determine if no more data available */
isNoMore?: (r: R | undefined) => boolean;
/** Distance from bottom to trigger load more (px) */
threshold?: number;
}Usage Examples:
import { useRef } from "react";
// Basic options with scroll container
const containerRef = useRef<HTMLDivElement>(null);
const posts = useRequest('/api/posts', {
loadMore: true,
ref: containerRef,
threshold: 300, // Load when 300px from bottom
isNoMore: (data) => data?.hasNext === false,
onSuccess: (data) => {
console.log(`Loaded ${data.list.length} new items`);
}
});
// With custom formatting and logic
const products = useRequest('/api/products', {
loadMore: true,
formatResult: (response) => ({
list: response.data.items,
hasNext: response.data.pagination.hasMore,
totalCount: response.data.pagination.total
}),
isNoMore: (data) => !data?.hasNext,
threshold: 500,
// Load more options inherit all BaseOptions
cacheKey: 'infinite-products',
refreshOnWindowFocus: true
});Complete load more result with accumulated data, loading states, and control functions.
interface LoadMoreResult<R> extends BaseResult<R, LoadMoreParams<R>> {
/** Indicates no more data available */
noMore?: boolean;
/** Load next batch of data */
loadMore: () => void;
/** Reload from beginning (reset accumulated data) */
reload: () => void;
/** Loading state for additional data */
loadingMore: boolean;
}Usage Examples:
const {
data, // Accumulated data { list: Item[], ...additionalData }
loading, // Initial loading state
loadingMore, // Loading additional data state
noMore, // No more data flag
loadMore, // Load next batch function
reload, // Reset and reload from start
error, // Error state
run, // Manual execution with parameters
refresh // Refresh current data
} = useRequest('/api/feed', {
loadMore: true,
isNoMore: (data) => data?.hasMore === false
});
// Manual load more trigger
const handleLoadMore = () => {
if (!loadingMore && !noMore) {
loadMore();
}
};
// Reset and start over
const handleReload = () => {
reload();
};
// Render accumulated data
return (
<div>
{data?.list?.map((item, index) => (
<div key={index}>{item.title}</div>
))}
{loadingMore && <div>Loading more...</div>}
{noMore && <div>No more data</div>}
<button onClick={handleLoadMore} disabled={loadingMore || noMore}>
Load More
</button>
<button onClick={handleReload}>
Reload
</button>
</div>
);import { useRef, useEffect } from "react";
const InfiniteScrollList = () => {
const scrollRef = useRef<HTMLDivElement>(null);
const { data, loadingMore, noMore } = useRequest(
(prevData) => {
const cursor = prevData?.nextCursor || 0;
return `/api/items?cursor=${cursor}&limit=20`;
},
{
loadMore: true,
ref: scrollRef,
threshold: 200, // Trigger when 200px from bottom
isNoMore: (data) => !data?.nextCursor
}
);
return (
<div
ref={scrollRef}
style={{
height: '500px',
overflowY: 'auto',
border: '1px solid #ccc'
}}
>
{data?.list?.map((item, index) => (
<div key={item.id || index} style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
{item.title}
</div>
))}
{loadingMore && (
<div style={{ padding: '20px', textAlign: 'center' }}>
Loading more items...
</div>
)}
{noMore && (
<div style={{ padding: '20px', textAlign: 'center', color: '#999' }}>
No more items to load
</div>
)}
</div>
);
};// For full-page infinite scroll (no container ref)
const { data, loadingMore, noMore } = useRequest(
(prevData) => {
const page = prevData ? prevData.currentPage + 1 : 1;
return `/api/news?page=${page}&size=10`;
},
{
loadMore: true,
// No ref provided - uses window scroll
threshold: 300,
isNoMore: (data) => data?.currentPage >= data?.totalPages
}
);
// Renders in document body with automatic window scroll detection// Custom data merging logic
const chatMessages = useRequest(
(prevData) => {
const beforeId = prevData?.oldestMessageId || null;
return `/api/chat/messages?before=${beforeId}&limit=50`;
},
{
loadMore: true,
formatResult: (response, prevData) => ({
// Prepend older messages (for chat history)
list: [...response.messages, ...(prevData?.list || [])],
oldestMessageId: response.messages[0]?.id,
hasOlderMessages: response.hasMore
}),
isNoMore: (data) => !data?.hasOlderMessages
}
);
// Custom sorting/filtering during accumulation
const searchResults = useRequest(
(prevData, query: string) => {
const offset = prevData?.list?.length || 0;
return `/api/search?q=${query}&offset=${offset}&limit=20`;
},
{
loadMore: true,
formatResult: (response, prevData) => {
const existingIds = new Set(prevData?.list?.map(item => item.id) || []);
const newItems = response.results.filter(item => !existingIds.has(item.id));
return {
list: [...(prevData?.list || []), ...newItems],
total: response.total,
hasMore: response.hasMore
};
},
isNoMore: (data) => !data?.hasMore
}
);const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('date');
const productList = useRequest(
(prevData, cat: string, sort: string) => {
const offset = prevData?.list?.length || 0;
return `/api/products?category=${cat}&sort=${sort}&offset=${offset}&limit=12`;
},
{
loadMore: true,
refreshDeps: [category, sortBy], // Reset when filters change
defaultParams: [category, sortBy], // Pass current filter values
isNoMore: (data) => data?.list?.length === 0 || data?.reachedEnd === true
}
);
// When filters change, data automatically resets and reloads
const handleCategoryChange = (newCategory: string) => {
setCategory(newCategory);
// refreshDeps will trigger automatic reload
};const robustInfiniteScroll = useRequest(
(prevData) => {
const page = prevData ? prevData.page + 1 : 1;
return `/api/content?page=${page}&size=15`;
},
{
loadMore: true,
isNoMore: (data) => data?.hasNext === false,
onError: (error, [prevData]) => {
console.error('Load more failed:', {
error: error.message,
attemptedPage: prevData ? prevData.page + 1 : 1,
currentItemCount: prevData?.list?.length || 0
});
// Could implement retry logic or show error UI
},
// Optionally retry on errors
retryCount: 3,
retryInterval: 1000
}
);
// Error recovery UI
const LoadMoreWithErrorHandling = () => {
const { data, loadingMore, noMore, error, loadMore } = robustInfiniteScroll;
return (
<div>
{data?.list?.map(item => (
<div key={item.id}>{item.content}</div>
))}
{error && (
<div className="error-banner">
<p>Failed to load more content: {error.message}</p>
<button onClick={loadMore}>Try Again</button>
</div>
)}
{!error && loadingMore && <div>Loading...</div>}
{!error && noMore && <div>All content loaded</div>}
</div>
);
};import { useMemo } from "react";
const OptimizedInfiniteScroll = () => {
const { data, loadingMore, noMore } = useRequest('/api/items', {
loadMore: true,
threshold: 400,
// Cache for better performance
cacheKey: 'infinite-items',
cacheTime: 10 * 60 * 1000, // 10 minutes
isNoMore: (data) => data?.hasMore === false
});
// Memoize heavy computations
const processedItems = useMemo(() => {
return data?.list?.map(item => ({
...item,
formattedDate: new Date(item.createdAt).toLocaleDateString(),
categoryLabel: getCategoryLabel(item.category)
})) || [];
}, [data?.list]);
return (
<div>
{processedItems.map((item, index) => (
<div key={item.id || index}>
<h3>{item.title}</h3>
<p>{item.formattedDate} • {item.categoryLabel}</p>
</div>
))}
{loadingMore && <LoadingSpinner />}
{noMore && <EndMessage />}
</div>
);
};// Combine load more with real-time data
const liveActivityFeed = useRequest(
(prevData) => {
const lastId = prevData?.list?.[prevData.list.length - 1]?.id || null;
return `/api/activity?after=${lastId}&limit=20`;
},
{
loadMore: true,
pollingInterval: 30000, // Poll for new items every 30 seconds
isNoMore: (data) => data?.hasMore === false,
formatResult: (response, prevData) => {
// Handle both polling (new items at start) and load more (old items at end)
if (!prevData) return response;
const existingIds = new Set(prevData.list.map(item => item.id));
const newItems = response.list.filter(item => !existingIds.has(item.id));
return {
list: [...newItems, ...prevData.list],
hasMore: response.hasMore
};
}
}
);