T
TanStack11mo ago
adverse-sapphire

Handle notFound in a component

Hey All, Currently building a dashboard app with Tanstack Start + Query, though I don't think this question is relevant to Start specifically. Is there a way to throw/navigate to the notFound component in a route component (not the loader)? I won't know specifically if the page is a 404 until the query finishes, as I am in an $id route and querying for that specific item. I'd like to throw notFound if the query returns null. Is there any way to get there with the useNavigate or useRouter hook? I don't see anything in the documentation about getting there from either of those hooks.
20 Replies
fair-rose
fair-rose11mo ago
why don't you query from the loader?
adverse-sapphire
adverse-sapphireOP11mo ago
I'm prefetching in the loader, but I don't want to block navigation by awaiting the full query from the loader.
fair-rose
fair-rose11mo ago
so you are using useSuspenseQuery in the route component?
adverse-sapphire
adverse-sapphireOP11mo ago
yep
fair-rose
fair-rose11mo ago
this will result in the same then pending component being shown whether loader takes time or query in component
adverse-sapphire
adverse-sapphireOP11mo ago
So if I were to use it in a deeper suspended component would that still do the same?
export const Route = createFileRoute("/$id")({
loader: ({ context, params }) => {
context.queryClient.prefetchQuery(myQueryOptions($id));
},
component: RouteComponent
});

function RouteComponent() {
return (
<div>
<p>Hello Route Component!</p>
<Suspense>
<SuspendedComponent />
</Suspense>
</div>
);
}

function SuspendedComponent() {
const params = Route.useParams();
const { data } = useSuspenseQuery(myQueryOptions(params.id));

return <p>{data}</p>;
}
export const Route = createFileRoute("/$id")({
loader: ({ context, params }) => {
context.queryClient.prefetchQuery(myQueryOptions($id));
},
component: RouteComponent
});

function RouteComponent() {
return (
<div>
<p>Hello Route Component!</p>
<Suspense>
<SuspendedComponent />
</Suspense>
</div>
);
}

function SuspendedComponent() {
const params = Route.useParams();
const { data } = useSuspenseQuery(myQueryOptions(params.id));

return <p>{data}</p>;
}
fair-rose
fair-rose11mo ago
no, it you handle suspense yourself it's a different story
adverse-sapphire
adverse-sapphireOP11mo ago
what would be the best way to throw not found in that scenario?
fair-rose
fair-rose11mo ago
a custom error component maybe? other than that we are currently exploring throwing redirects from components notFound is similar
adverse-sapphire
adverse-sapphireOP11mo ago
got it, that makes sense to me. Thanks for clearing up the pendingComponent/suspense thing, I can probably change a lot of my loaders to ensureQueryData and use pendingComponent
fair-rose
fair-rose11mo ago
yes that's the recommended way
adverse-sapphire
adverse-sapphireOP11mo ago
that sounds awesome. thanks!
fair-rose
fair-rose11mo ago
the only downside of that approach is that you will get "uncaught errors" being logged although they are caught but no way to work around this with react so far
adverse-sapphire
adverse-sapphireOP11mo ago
got it thanks for answering so quickly!
deep-jade
deep-jade9mo ago
Hey @alrightsure, can you share if there is a solution you have implemented on this issue?
adverse-sapphire
adverse-sapphireOP9mo ago
yea I mean bascially what manuel said. Fetch in the loader, throw not found there.
deep-jade
deep-jade9mo ago
Do you block navigation in loader with async/await?
adverse-sapphire
adverse-sapphireOP9mo ago
ya you'd have to
deep-jade
deep-jade9mo ago
Since I don't want to block navigation, I added CatchNotFound component to the root route like this.
import { NotFoundError } from '@/components/errors/not-found';
import type { QueryClient } from '@tanstack/react-query';
import {
CatchNotFound,
Outlet,
createRootRouteWithContext,
} from '@tanstack/react-router';

export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>()({
component: RootComponent,
});

function RootComponent() {
return (
<CatchNotFound fallback={NotFoundError}>
<Outlet />
</CatchNotFound>
);
}
import { NotFoundError } from '@/components/errors/not-found';
import type { QueryClient } from '@tanstack/react-query';
import {
CatchNotFound,
Outlet,
createRootRouteWithContext,
} from '@tanstack/react-router';

export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>()({
component: RootComponent,
});

function RootComponent() {
return (
<CatchNotFound fallback={NotFoundError}>
<Outlet />
</CatchNotFound>
);
}
My other route is like this and uses useSuspenseQuery. When a 404 error is caught in the axios instance, the notFound() function is thrown. This way I can catch 404 errors without blocking navigation. But I have two problems; - I can't use properties like notFoundMode because it's always the CatchNotFound component that catches the error and it's always rendered in the root component. - When a notFound error is thrown, the defaultErrorComponent component is also rendered. So actually the component content is not rendered but I can see the console.logs written in the component.
export const Route = createFileRoute(
'/_protected/(doc)/sessions/$sessionId/documents/$documentId',
)({
loader: ({ context, params }) => {
context.queryClient.ensureQueryData(
getApiViewsDocumentsGetDocumentDetailsQueryOptions(params.documentId),
);
},
component: RouteComponent,
});

function RouteComponent() {
const params = Route.useParams({
select: (params) => ({
documentId: params.documentId,
}),
});

const { data } = useApiViewsDocumentsGetDocumentDetailsSuspense(
params.documentId,
);

return <Document document={data} />;
}
export const Route = createFileRoute(
'/_protected/(doc)/sessions/$sessionId/documents/$documentId',
)({
loader: ({ context, params }) => {
context.queryClient.ensureQueryData(
getApiViewsDocumentsGetDocumentDetailsQueryOptions(params.documentId),
);
},
component: RouteComponent,
});

function RouteComponent() {
const params = Route.useParams({
select: (params) => ({
documentId: params.documentId,
}),
});

const { data } = useApiViewsDocumentsGetDocumentDetailsSuspense(
params.documentId,
);

return <Document document={data} />;
}

Did you find this page helpful?