T
TanStack9mo ago
sensitive-blue

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
correct-apricot
correct-apricot9mo ago
why don't you query from the loader?
sensitive-blue
sensitive-blueOP9mo ago
I'm prefetching in the loader, but I don't want to block navigation by awaiting the full query from the loader.
correct-apricot
correct-apricot9mo ago
so you are using useSuspenseQuery in the route component?
sensitive-blue
sensitive-blueOP9mo ago
yep
correct-apricot
correct-apricot9mo ago
this will result in the same then pending component being shown whether loader takes time or query in component
sensitive-blue
sensitive-blueOP9mo 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>;
}
correct-apricot
correct-apricot9mo ago
no, it you handle suspense yourself it's a different story
sensitive-blue
sensitive-blueOP9mo ago
what would be the best way to throw not found in that scenario?
correct-apricot
correct-apricot9mo ago
a custom error component maybe? other than that we are currently exploring throwing redirects from components notFound is similar
sensitive-blue
sensitive-blueOP9mo 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
correct-apricot
correct-apricot9mo ago
yes that's the recommended way
sensitive-blue
sensitive-blueOP9mo ago
that sounds awesome. thanks!
correct-apricot
correct-apricot9mo 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
sensitive-blue
sensitive-blueOP9mo ago
got it thanks for answering so quickly!
frozen-sapphire
frozen-sapphire6mo ago
Hey @alrightsure, can you share if there is a solution you have implemented on this issue?
sensitive-blue
sensitive-blueOP6mo ago
yea I mean bascially what manuel said. Fetch in the loader, throw not found there.
frozen-sapphire
frozen-sapphire6mo ago
Do you block navigation in loader with async/await?
sensitive-blue
sensitive-blueOP6mo ago
ya you'd have to
frozen-sapphire
frozen-sapphire6mo 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?