T
TanStack10mo ago
stormy-gold

Query with Pagination + Filters

I'm implementing pagination with filters using React Query, and I've hit an interesting UX issue. Here's the scenario:
// hooks/usePatients.ts
export function usePatients({
initialData,
filters,
skip,
take,
}: {
initialData?: Awaited<TFetchResponse<Patient>>;
skip?: number;
take?: number;
filters?: PatientFilters;
}) {
return useQuery({
queryKey: ['patients', { skip }, { take }, { filters: JSON.stringify(filters) }],
queryFn: async () => {
const response = await patientFindManyAction({
skip,
take,
filters,
});

if (!response || !response.data) throw new Error('Something went wrong');
return response.data;
},
placeholderData: (previousData) => previousData ? previousData : initialData,
});
}
// hooks/usePatients.ts
export function usePatients({
initialData,
filters,
skip,
take,
}: {
initialData?: Awaited<TFetchResponse<Patient>>;
skip?: number;
take?: number;
filters?: PatientFilters;
}) {
return useQuery({
queryKey: ['patients', { skip }, { take }, { filters: JSON.stringify(filters) }],
queryFn: async () => {
const response = await patientFindManyAction({
skip,
take,
filters,
});

if (!response || !response.data) throw new Error('Something went wrong');
return response.data;
},
placeholderData: (previousData) => previousData ? previousData : initialData,
});
}
continue...
2 Replies
stormy-gold
stormy-goldOP10mo ago
// PatientList.tsx
const {
page,
pageSize,
skip,
nextPage,
previousPage,
setPageNumber,
handleSetPageSize,
} = usePagination();


//when the filters change, reset the page number
useEffect(() => {
setPageNumber(0);
}, [filterState]);

const { data, isSuccess, isError } = usePatientFindMany({
initialData,
filters: debouncedFilterState,
skip,
take: pageSize,
});

useEffect(() => {
if (isError) {
toast.error('Error loading patients');
}
}, [isError]);

if (!isSuccess) return null;

return (
<div className={cn('flex h-full flex-col gap-2 border-8', className)}>
<PatientListFilterBar
filters={filterState}
// @ts-expect-error
setFilters={setFilterState}
/>
<PatientGrid patients={data.data} className='flex-grow' />
<ListPagination
className='sticky bottom-2'
setPageSize={handleSetPageSize}
pageNumber={page}
pageCount={Math.ceil(data.count / pageSize)}
setPageNumber={setPageNumber}
pageSize={pageSize}
nextPage={nextPage}
previousPage={previousPage}
/>
</div>
);
}
// PatientList.tsx
const {
page,
pageSize,
skip,
nextPage,
previousPage,
setPageNumber,
handleSetPageSize,
} = usePagination();


//when the filters change, reset the page number
useEffect(() => {
setPageNumber(0);
}, [filterState]);

const { data, isSuccess, isError } = usePatientFindMany({
initialData,
filters: debouncedFilterState,
skip,
take: pageSize,
});

useEffect(() => {
if (isError) {
toast.error('Error loading patients');
}
}, [isError]);

if (!isSuccess) return null;

return (
<div className={cn('flex h-full flex-col gap-2 border-8', className)}>
<PatientListFilterBar
filters={filterState}
// @ts-expect-error
setFilters={setFilterState}
/>
<PatientGrid patients={data.data} className='flex-grow' />
<ListPagination
className='sticky bottom-2'
setPageSize={handleSetPageSize}
pageNumber={page}
pageCount={Math.ceil(data.count / pageSize)}
setPageNumber={setPageNumber}
pageSize={pageSize}
nextPage={nextPage}
previousPage={previousPage}
/>
</div>
);
}
The issue is the sequence of events when filters change: 1. User applies new filter -> the effect runs 2. New query starts fetching 3. Page resets to 1 4. User sees page 1 of OLD results 5. New filtered results arrive and replace the view This creates a noticeable flicker: OLD_DATA (page N) → OLD_DATA (page 1) → NEW_DATA (page 1) Any ideas how to handle this properly? is there any good example of data with pagination and filters working flawlessly? Thank you! @TkDodo 🔮 I'd really appreciate your insights on this, as I'm completely stuck...!
metropolitan-bronze
metropolitan-bronze10mo ago
triggering the setPageNumber in an useEffect is a bad pattern, you wait for filterState to change in one render, then wait for page to change in another, instead do
setFilters={(...args) => {
setPageNumber(0);
setFilterState(...args);
}}
setFilters={(...args) => {
setPageNumber(0);
setFilterState(...args);
}}
to batch the 2 set states, which should result in only 1 new query being triggered (i used ...args because I do not know what setFilters and setFilterState expect, I suppose it is a filter object)

Did you find this page helpful?