import { useCallback, useEffect, useRef, useState } from "react"; import { usePixel } from "./usePixel"; export interface UseIteratorPixelOptions { /** Number of items to fetch per page */ limit?: number; /** Insight ID to use for pixel calls; defaults to the app's shared insight */ insightId?: string; /** Callback when data is successfully loaded */ onSuccess?: (data: T[], isLoadingMore: boolean) => void; /** Callback when an error occurs */ onError?: (error: Error) => void; } export interface UseIteratorPixelReturn { /** Current accumulated data */ data: T[]; /** Total count of the data */ totalCount: number; /** Track if errored */ isError: boolean; /** Error if status is ERROR */ error: Error | null; /** Whether currently loading */ isLoading: boolean; /** Whether there are more items to load */ hasMore: boolean; /** Load the next set of items */ next: () => void; /** Reset the state */ reset: () => void; } /** * Hook for handling paginated/iterator pixel queries with automatic accumulation * * @param pixelQuery - Function that generates the pixel query with limit and offset * @param getTotalCount - Function to extract total count from response * @param getData - Function to extract data array from response * @param dependencies - Dependencies that trigger a reset when changed * @param options - Configuration options * * @example * ```tsx * const { data, hasMore, next, isLoading } = useIteratorPixel( * (limit, offset) => `GetWorkspaceRooms(workspaceId=["${id}"], limit=[${limit}], offset=[${offset}]);`, * (response) => response.total_count, * (response) => response.rooms, * { limit: 25 }, * [id] * ); * ``` */ export function useIteratorPixel( pixelQuery: (limit: number, offset: number) => string, getTotalCount: (response: TResponse) => number, getData: (response: TResponse) => TItem[], options: UseIteratorPixelOptions = {}, dependencies: React.DependencyList = [], ): UseIteratorPixelReturn { const { limit = 25, insightId, onSuccess, onError } = options; const [offset, setOffset] = useState(0); const [allData, setAllData] = useState([]); const [totalCount, setTotalCount] = useState(0); const isLoadingMoreRef = useRef(false); // Generate the pixel query const query = pixelQuery(limit, offset); // Use the standard usePixel hook const pixel = usePixel( query, { data: null as TResponse, onSuccess: (response) => { const newData = getData(response); const count = getTotalCount(response); setTotalCount(count); if (offset === 0) { // First load or reset - replace all data setAllData(newData); } else { // Loading more - append data setAllData((prev) => [...prev, ...newData]); } if (onSuccess) { onSuccess(newData, offset > 0); } }, onError: (_data, error) => { if (onError && error instanceof Error) { onError(error); } }, onFinal: () => { isLoadingMoreRef.current = false; }, }, insightId, ); /** * Get the next set of items */ const next = useCallback(() => { if ( !isLoadingMoreRef.current && pixel.status !== "LOADING" && allData.length < totalCount ) { isLoadingMoreRef.current = true; setOffset((prev) => prev + limit); } }, [pixel.status, allData.length, totalCount, limit]); /** * Reset the state */ const reset = useCallback(() => { setOffset(0); isLoadingMoreRef.current = false; // get the data pixel.refresh(); }, [pixel.refresh]); /** * Reset when dependencies change */ useEffect( () => { setOffset(0); setAllData([]); setTotalCount(0); isLoadingMoreRef.current = false; }, // biome-ignore lint/correctness/useExhaustiveDependencies: suppress exhaustive dependencies check dependencies, ); return { data: allData, totalCount: totalCount, isError: pixel.status === "ERROR", error: pixel.error, isLoading: pixel.status === "LOADING" || isLoadingMoreRef.current, hasMore: allData.length < totalCount, next: next, reset: reset, }; }