or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdcore-request.mdindex.mdload-more.mdpagination.md
tile.json

load-more.mddocs/

Load More / Infinite Scroll

Infinite scroll functionality with automatic loading triggers, scroll position recovery, data accumulation, and comprehensive loading state management for seamless user experiences.

Capabilities

Load More useRequest

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>;

useLoadMore

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
  }
);

Data Types

Load More Parameters

/**
 * 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;
}

Configuration Options

Basic Load More Options

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;
}

Load More with Formatting

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
});

Result Interface

LoadMoreResult

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>
);

Automatic Scroll Detection

Scroll Container Setup

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>
  );
};

Window Scroll Detection

// 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

Advanced Usage

Custom Data Accumulation

// 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
  }
);

Load More with Parameters

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
};

Error Handling and Retry

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>
  );
};

Performance Optimization

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>
  );
};

Load More with Real-time Updates

// 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
      };
    }
  }
);