T
TanStack3y ago
rising-crimson

tanstack router + query + searchParams = keepPreviousData does not work

Hey 👋🏽 , thanks for this great project. I'm using tanstack/router and tanstack/query. The filter + pagination state should be in searchParams. At the query I use keepPreviousData: true, but if a page is not in cache, I get a loading-state. What I'm doing wrong?
export const participantsRoute = new Route({
path: "/participants",
getParentRoute: () => rootRoute,
component: ParticipantsPage,
});


export function ParticipantsPage({ useSearch }) {
const { page, sort } = useSearch({ from: "/participants" });

const { data, isLoading } = useParticipantsQuery({ page, sort });

if (isLoading) {
return <Loading size="lg" />;
}

return <ParticipantTable data={data} page={page} />;
}
export const participantsRoute = new Route({
path: "/participants",
getParentRoute: () => rootRoute,
component: ParticipantsPage,
});


export function ParticipantsPage({ useSearch }) {
const { page, sort } = useSearch({ from: "/participants" });

const { data, isLoading } = useParticipantsQuery({ page, sort });

if (isLoading) {
return <Loading size="lg" />;
}

return <ParticipantTable data={data} page={page} />;
}
The page switch is done via a navigation:
const goToPage = ({ number }) => {
navigate({
search: (prev) => {
return { ...prev, page: number };
},
});
};
const goToPage = ({ number }) => {
navigate({
search: (prev) => {
return { ...prev, page: number };
},
});
};
Thanks
9 Replies
correct-apricot
correct-apricot3y ago
you're not showing useParticipantsQuery. a codesandbox would be best
rising-crimson
rising-crimsonOP3y ago
Thanks for the response. Here's the useParticipantsQuery. Is the issue, that the ParticipantsPage is rerendered after the navigation? If yes, do you know a workaround?
const query = gql`
query FilterParticipants(
$after: String
$first: Int
$query: ParticipantQueryInput!
) {
filterParticipants(after: $after, first: $first, query: $query) {
totalCount
nodes {
...Participant
}
}
}
${Participant}
`;

const afterForPage = (page, itemsPerPage) => String((page - 1) * itemsPerPage);

const pageParams = (page, itemsPerPage) => ({
after: afterForPage(page, itemsPerPage),
first: itemsPerPage,
});

export function useParticipantsQuery({
page = 1,
sort = [],
itemsPerPage = 10,
} = {}) {
return useQuery({
queryKey: ["participants", "list", sort, page],
keepPreviousData: true,
queryFn: async () => {
return request(import.meta.env.VITE_GQL_ENDPOINT, query, {
...pageParams(page, itemsPerPage),
query: { filter: {}, sort },
}).then((data) => ({
totalPages: Math.ceil(
data.filterParticipants.totalCount / itemsPerPage
),
records: data.filterParticipants.nodes,
}));
},
});
}
const query = gql`
query FilterParticipants(
$after: String
$first: Int
$query: ParticipantQueryInput!
) {
filterParticipants(after: $after, first: $first, query: $query) {
totalCount
nodes {
...Participant
}
}
}
${Participant}
`;

const afterForPage = (page, itemsPerPage) => String((page - 1) * itemsPerPage);

const pageParams = (page, itemsPerPage) => ({
after: afterForPage(page, itemsPerPage),
first: itemsPerPage,
});

export function useParticipantsQuery({
page = 1,
sort = [],
itemsPerPage = 10,
} = {}) {
return useQuery({
queryKey: ["participants", "list", sort, page],
keepPreviousData: true,
queryFn: async () => {
return request(import.meta.env.VITE_GQL_ENDPOINT, query, {
...pageParams(page, itemsPerPage),
query: { filter: {}, sort },
}).then((data) => ({
totalPages: Math.ceil(
data.filterParticipants.totalCount / itemsPerPage
),
records: data.filterParticipants.nodes,
}));
},
});
}
correct-apricot
correct-apricot3y ago
well keepPreviousData only works if a page re-renders and the observer previously had data. if you mount a new page, there is no "previous data" to keep
rising-crimson
rising-crimsonOP3y ago
So searchParams cannot be changed without remounting the page?
correct-apricot
correct-apricot3y ago
I don't know ?
rising-crimson
rising-crimsonOP3y ago
Ok, thanks! @TkDodo 🔮 Maybe the way to go is to use the load ft of the route. What do you think about it?
export const participantsRoute = new Route({
path: "/participants",
load: ({ search, context: { queryClient } }) => {
return queryClient.ensureQueryData({
queryKey: ["participants", "list", search.page],
});
},
getParentRoute: () => rootRoute,
component: ParticipantsPage,
});
export const participantsRoute = new Route({
path: "/participants",
load: ({ search, context: { queryClient } }) => {
return queryClient.ensureQueryData({
queryKey: ["participants", "list", search.page],
});
},
getParentRoute: () => rootRoute,
component: ParticipantsPage,
});
ensureQueryData needs also a queryFn-prop. In my case, I only want to ensure data if the query already exists. I therefore do not want to provide a queryFn. Is that possible?
correct-apricot
correct-apricot3y ago
In my case, I only want to ensure data if the query already exists.
what does that mean? "ensure" means to make sure that something exists. If the query doesn't exist yet, ensureQueryData will fetch so that after you await ensureQueryData(), you can be sure that data is in the cache 😅 if the query already exsists, it just reads that from the cache. From what you are describing, it sounds like you want the normal default behaviour of useQuery without any loaders ... if the loaders don't load data for queries that don't yet exist, what are they doing? I think I'm just misunderstanding you though
rising-crimson
rising-crimsonOP3y ago
I understand what you mean. I am currently at a crossroads as to whether to keep the filterParams as searchParams or to store them in zustand. SearchParams sounds tempting, as it improves the UX considerably (browser histroy), but the DX is still lower at the moment (at least for me). Remounting the page on every filtering/page-change feels inefficient. Or is that pretty much irrelevant in React? Maybe I lack internal knowledge to answer this question. Do you have any input? What I want to achieve here is simply the great tanstack-query-pagination-behavior (keepPreviousData) that I can't manage with the SearchParams.
rising-crimson
rising-crimsonOP3y ago
@TkDodo 🔮 I took another close look. It seems to work now. Checkout: https://stackblitz.com/edit/tanstack-router-ni9orz?file=src%2Fmain.tsx My mistake was using keepPreviousData in v5 🙈🙈🙈🙈🙈🙈🙈🙈
Armin
StackBlitz
pagination-with-search-params - StackBlitz
Run official live example code for Router Basic, created by Tanstack on StackBlitz

Did you find this page helpful?