T
TanStack3mo ago
metropolitan-bronze

keepPreviousData with useSuspenseQuery

Hello, I'm using router with query and was wondering if this pattern can be done in a better way: I want to have data available right away on the inital load so I don't need to implement loading screens, but on client navigation I just want to use previous data until the new one is fetched. when using useSuspenseQuery + awaiting ensureQueryData in loader it works that way but since my UI depends on search params when changing route I still see previous params until loader finishes fetching the data. In my case im using a select component like this:
const {value} = Route.useSearch()
const navigate = Route.useNavigate()

return (
<div>
<Select currentValue={value} onChange={(val) => navigate({to: "/", search: {val}})}
</div>
)
const {value} = Route.useSearch()
const navigate = Route.useNavigate()

return (
<div>
<Select currentValue={value} onChange={(val) => navigate({to: "/", search: {val}})}
</div>
)
I see 2 potential solutions but they both seem non-ideal 1. Using prefetch query in loaders but awaiting it only on the server and then using useQuery + keepPreviousData, the issue here is that the data is still T | undefined 2. Detaching UI state from search params Is there a 3rd better way that I'm missing?
2 Replies
exotic-emerald
exotic-emerald3mo ago
Not sure if this is the best way to solve this, but I ran into a similar issue to this recently, and this was my way of solving it. Maybe it helps!
export const Route = createFileRoute('/data/')({
component: RouteComponent,
loaderDeps: ({ search }) => ({ search }),
loader: async ({
deps: { search },
cause,
}) => {
const dataWithPromise = fetch(...);

if (cause === "stay") {
return {
dataWithPromise,
};
}

const data = await dataWithPromise;
return {
data,
};
},
});

function RouteComponent() {
const {
data,
dataWithPromise,
} = Route.useLoaderData();

if (dataWithPromise) {
return (
<Await
promise={dataWithPromise}
fallback={<div>Loading...</div>}
>
{(data) => (
<div>{data}</div>
)}
</Await>
)
}

return (
<div>
<div>
{data}
</div>
</div>
)
}
export const Route = createFileRoute('/data/')({
component: RouteComponent,
loaderDeps: ({ search }) => ({ search }),
loader: async ({
deps: { search },
cause,
}) => {
const dataWithPromise = fetch(...);

if (cause === "stay") {
return {
dataWithPromise,
};
}

const data = await dataWithPromise;
return {
data,
};
},
});

function RouteComponent() {
const {
data,
dataWithPromise,
} = Route.useLoaderData();

if (dataWithPromise) {
return (
<Await
promise={dataWithPromise}
fallback={<div>Loading...</div>}
>
{(data) => (
<div>{data}</div>
)}
</Await>
)
}

return (
<div>
<div>
{data}
</div>
</div>
)
}
Essentially anything after the initial load uses deferred data loading (https://tanstack.com/router/latest/docs/framework/react/guide/deferred-data-loading) -- that also helped me solve the issue where searchParams state would only update AFTER the loader finishes. This way it immediately updates pretty much in case you have some buttons with like active state or something.
metropolitan-bronze
metropolitan-bronzeOP3mo ago
I think i will go with something like this for now:
const search = Route.useSearch()
const navigate = Route.useNavigate()

const [optimisticSearch, setOptimisticSearch] = useOptimistic(search)
const updateSearch = (search: Parameters<typeof setOptimisticSearch>[0]) => {
startTransition(async () => {
setOptimisticSearch(search)
await navigate({ search })
})
}
const search = Route.useSearch()
const navigate = Route.useNavigate()

const [optimisticSearch, setOptimisticSearch] = useOptimistic(search)
const updateSearch = (search: Parameters<typeof setOptimisticSearch>[0]) => {
startTransition(async () => {
setOptimisticSearch(search)
await navigate({ search })
})
}
And then just keeping useSuspenseQuery + awaiting ensureQueryData. I'm still not 100% convinced it's the best way but it seems less hacky than the previous solutions. I will still really appreciate any suggestions

Did you find this page helpful?