Infinite Scroll
FloppyDisk does not provide a dedicated "infinite query" API for infinite scroll UI.
Instead, it embraces a simpler and more flexible approach:
Infinite queries can be easily built with composition + recursive rendering.
Why? Because async state is already powerful enough:
- keyed queries handle parameters
- components handle composition
- recursive rendering handles pagination
No special abstraction needed.
Instead of relying on a specific API, infinite scroll is achieved by composing components that render the next page when needed. Each page is simply another component instance with a different cursor.
How It Works
At a high level, the pattern looks like this:
Page → LoadMore → Page → LoadMore → ...Pageis responsible for fetching and rendering a single page of dataLoadMoredecides when to render the next page- When triggered,
LoadMorerenders anotherPagewith the next cursor
This creates a recursive rendering pattern where each page naturally leads to the next.
Here is the example on how to implement infinite query properly:
type GetPlantParams = {
cursor?: string; // For pagination
};
type GetPlantsResponse = {
plants: Plant[];
meta: { nextCursor: string };
};
const plantsQuery = createQuery<GetPlantsResponse, GetPlantParams>(getPlants, {
staleTime: Infinity,
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
function Main() {
return <Page cursor={undefined} />;
}
function Page({ cursor }: { cursor?: string }) {
const usePlantsQuery = plantsQuery({ cursor });
const { data, error } = usePlantsQuery();
if (!data && !error) return <div>Loading...</div>;
if (error) return <div>Error</div>;
return (
<>
{data.plants.map((plant) => (
<PlantCard key={plant.id} plant={plant} />
))}
{data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
</>
);
}
function LoadMore({ nextCursor }: { nextCursor?: string }) {
const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
const stateOfNextPageQuery = plantsQuery({ cursor: nextCursor }).getState();
return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
});
if (isNextPageRequested) {
return <Page cursor={nextCursor} />;
}
return <DomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
}Revalidation Considerations
When implementing infinite queries, it is highly recommended to disable automatic revalidation.
Why?
In an infinite list, users may scroll through many pages ("doom-scrolling").
If revalidation is triggered:
- All previously loaded pages may re-execute
- Content at the top may change without the user noticing
- Layout shifts can occur unexpectedly
This leads to a confusing and unstable user experience.
Revalidating dozens of previously viewed pages rarely provides value to the user.