T
TanStack2mo ago
absent-sapphire

Defer viewTransition until component can render without suspending

I don't know if this is supposed to work like this, but I thought the point of view transitions was to hide the loading state? I tried:
export const Route = createFileRoute("/calendar")({
validateSearch: zodValidator(searchSchema),
beforeLoad: ({ search }) => {
return {
queries: {
calendarEventsUpcoming: calendarEventsUpcomingQuery(search.filter),
calendarEventsCompleted: calendarEventsQuery(search.filter),
},
};
},
loader: async ({ context: { queryClient, queries } }) => {
const [upcomingEvents, completedEvents] = await Promise.all([
queryClient.ensureQueryData(queries.calendarEventsUpcoming),
queryClient.ensureQueryData(queries.calendarEventsCompleted),
]);
return { upcomingEvents, completedEvents };
// ...

function RouteComponent() {
const { queries } = useRouteContext({ from: "/calendar" });
const { data: upcomingData } = useSuspenseQuery(
queries.calendarEventsUpcoming,
);
const { data: completedData } = useSuspenseQuery(
queries.calendarEventsCompleted,
);
const navigate = useNavigate({ from: "/incidents/retrospectives" });
const search = useSearch({ from: "/incidents/retrospectives" });

const handleFilter = (value: string) => {
void navigate({
search: {
filter: value,
},
viewTransition: true,
});
};
export const Route = createFileRoute("/calendar")({
validateSearch: zodValidator(searchSchema),
beforeLoad: ({ search }) => {
return {
queries: {
calendarEventsUpcoming: calendarEventsUpcomingQuery(search.filter),
calendarEventsCompleted: calendarEventsQuery(search.filter),
},
};
},
loader: async ({ context: { queryClient, queries } }) => {
const [upcomingEvents, completedEvents] = await Promise.all([
queryClient.ensureQueryData(queries.calendarEventsUpcoming),
queryClient.ensureQueryData(queries.calendarEventsCompleted),
]);
return { upcomingEvents, completedEvents };
// ...

function RouteComponent() {
const { queries } = useRouteContext({ from: "/calendar" });
const { data: upcomingData } = useSuspenseQuery(
queries.calendarEventsUpcoming,
);
const { data: completedData } = useSuspenseQuery(
queries.calendarEventsCompleted,
);
const navigate = useNavigate({ from: "/incidents/retrospectives" });
const search = useSearch({ from: "/incidents/retrospectives" });

const handleFilter = (value: string) => {
void navigate({
search: {
filter: value,
},
viewTransition: true,
});
};
But in my case, this triggers a view transition into my defaultPendingComponent before it does a contentful paint. I thought it would wait with the view transition until the component can render without suspending
9 Replies
exotic-emerald
exotic-emerald2mo ago
I ran into this recently. I found that a combination of preloading (to reduce loader times after navigate) and setting “pendingMs” to a reasonable value for your endpoints (200-500ms) should more or less fix it, since it won’t show your loader unless it’s been waiting for at least that long. For view transitions, it seems like you would want to prevent the loader showing unless absolutely necessary
absent-sapphire
absent-sapphireOP2mo ago
hmm so with
pendingMinMs: 1000,
pendingMs: 500,
pendingMinMs: 1000,
pendingMs: 500,
I still see a flash of my loading component
absent-sapphire
absent-sapphireOP2mo ago
my devtools are not capturing it as a screenshot but I swear it flashes my loading component in that period with partially presented frames. Not sure if there's a better way to debug this stuff
No description
absent-sapphire
absent-sapphireOP2mo ago
actually pendingMinMs does not apply when navigate()ing to the same page you're on the route loader does get called before render but TSR does not wait for the loader to resolve before calling my render, and it does not wait for my suspenseful render to finish before view transitioning
like-gold
like-gold2mo ago
this probably means you have a cached match with stale data, so the loader runs in the background asynchronously do you have a complete example project to show the issue?
absent-sapphire
absent-sapphireOP2mo ago
too deeply embedded in my closed source app 😦 you're talking about tsr route cache, can I try something to make it feel the need to refresh and wait for the loader to finish? I use tanstack query and tried replacing my ensureQueryData which return early with stale data primed, adding an explicit await on setTimeout in the loader, it's just not waiting for it
like-gold
like-gold2mo ago
you're talking about tsr route cache
yes if you use query, you dont use the route cache
it does not wait for my suspenseful render to finish before view transitioning
what does this mean exactly? ideally, can you modify one of the existing stackblitz examples as a reproducer? eg. https://tanstack.com/router/latest/docs/framework/react/examples/basic-react-query-file-based
absent-sapphire
absent-sapphireOP2mo ago
if you use query, you dont use the route cache
Why not? I still want hovering the link to pre-fetch data on that page. That's why I've been using ensureQueryData in these loaders.
it does not wait for my suspenseful render to finish before view transitioning what does this mean exactly?
I mean that loader() is called but then render() is also called after with the new search params, before the loader() call promise resolves. I'll try to repro but I can't spare the time this week, hopefully the next
like-gold
like-gold2mo ago
Why not? I still want hovering the link to pre-fetch data on that page. That's why I've been using ensureQueryData in these loaders.
because you are using query's cache for that. not router's

Did you find this page helpful?