router icon indicating copy to clipboard operation
router copied to clipboard

Route with server function as loader that throws `notFound` crashes route on HMR

Open ulrichstark opened this issue 1 month ago • 8 comments

Which project does this relate to?

Start

Describe the bug

I have a route that declares a server function as loader and a notFoundComponent. The server function throws notFound().

import { createFileRoute, notFound } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";

const loaderServerFn = createServerFn().handler(() => {
  throw notFound();
});

export const Route = createFileRoute("/")({
  loader: async () => await loaderServerFn(),
  component: RouteComponent,
  notFoundComponent: NotFoundComponent,
});

function NotFoundComponent() {
  return <span>NotFoundComponent</span>;
}

function RouteComponent() {
  return <span>RouteComponent</span>;
}

It shows NotFoundComponent as expected when initially navigating to it. When saving the file again in VSCode and therefore triggering an hot module replacement event in the browser, the route crashes with following error:

{"isNotFound":true}

Your Example Website or App

https://github.com/ulrichstark/tanstack-repro-isNotFound-on-hmr

Steps to Reproduce the Bug or Issue

git clone https://github.com/ulrichstark/tanstack-repro-isNotFound-on-hmr
cd tanstack-repro-isNotFound-on-hmr
npm i
npx vite

Then go to /src/routes/index.tsx and hit save to trigger hmr.

Expected behavior

NotFoundComponent should still be shown after hmr.

Screenshots or Videos

No response

Platform

  • Router / Start Version: 1.134.12

Additional context

Maybe similar to #5322

ulrichstark avatar Nov 05 '25 16:11 ulrichstark

A summary of the changes CodeRabbit can apply:

  • Update packages/router-core/src/router.ts to treat 'notFound' like 'error' in invalidate (change condition to d.status === 'error' || d.status === 'notFound') and add a new test in packages/react-router/tests/router.test.tsx verifying routes throwing notFound() are correctly reset and display notFoundComponent after HMR invalidation.

  • Add a new test in packages/react-router/tests/router.test.tsx that verifies a route returning a 404 notFound (via loader) continues to render its notFoundComponent after router.invalidate(), and update packages/router-core/src/router.ts to treat routes with status 'notFound' as invalidatable (include 'notFound' in the condition that resets a route to pending on invalidate).

  • [ ] ✅ Create PR with these edits
  • [ ] 📋 Get copyable edits

coderabbitai[bot] avatar Nov 05 '25 16:11 coderabbitai[bot]

@ulrichstark , there will be a release out in a minute that resolves this. Let us know if it works as expected.

birkskyum avatar Nov 19 '25 16:11 birkskyum

@ulrichstark , there will be a release out in a minute that resolves this. Let us know if it works as expected.

Hey thanks for letting me know. My issue isn't fixed by the most recent TanStack Start version. I just upgraded my repro project to version 1.137.0 of @tanstack/react-start. It's stil showing me {"isNotFound":true} after hitting save in index.tsx

ulrichstark avatar Nov 20 '25 07:11 ulrichstark

Also the same issue is happening when clicking a <Link> that navigates to a route with a server function throwing notFound().

I added a test route to my repro project. If you are on http://localhost:3000/link and click the link, you will see the same error like you would when saving the index.tsx route.

ulrichstark avatar Nov 20 '25 11:11 ulrichstark

This might be related to my issue with SSR + nested routes

I'm experiencing what could be related to this issue. In my case, I have a nested route structure where throwing notFound() in a child route's loader doesn't properly render the parent layout's notFoundComponent.

My Setup

I have a parent layout route with a notFoundComponent defined, and a child route that throws notFound() in its loader:

Parent route (/backoffice/clients/$clientId/layout.tsx):

export const Route = createFileRoute('/backoffice/clients/$clientId')({
  component: ClientLayout,
  notFoundComponent: ClientNotFound, // ← Defined here
  // ... other config
})

function ClientLayout() {
  return (
    <div>
      <DetailsNav>
        <Outlet />
      </DetailsNav>
    </div>
  )
}

Child route (/backoffice/clients/$clientId/personal-info.tsx):

export const Route = createFileRoute(
  '/backoffice/clients/$clientId/personal-info',
)({
  loader: async ({ context: { queryClient }, params: { clientId } }) => {
    // Client doesn't exist, throw notFound
    throw notFound()
  },
  component: PersonalInfo,
})

The Problem

Client-side navigation (renders wrong component ⚠️):

  1. Start at /backoffice/clients
  2. Click a link to navigate to /backoffice/clients/00000000-0000-0000-0000-000000000001/personal-info
  3. Result: Renders the root route's defaultNotFoundComponent instead of the layout's ClientNotFound component
  4. The parent layout's notFoundComponent is completely ignored

Direct navigation / SSR (complete failure ❌):

  1. Paste URL directly: http://localhost:3000/backoffice/clients/00000000-0000-0000-0000-000000000001/personal-info
  2. Result: Server responds with Cannot GET /backoffice/clients/00000000-0000-0000-0000-000000000001/personal-info
  3. No component is rendered at all - just a blank error page

Environment

  • @tanstack/react-router: ^1.139.3
  • @tanstack/react-start: ^1.139.3
  • Using Nitro (TanStack Start default server)

mailok avatar Nov 23 '25 07:11 mailok

I can confirm this issue with TanStack Start version 1.139.3.

Setup:

  • notFoundComponent configured in __root.tsx and _authed.tsx
  • Using throw notFound() in loader

Symptoms:

  • ✅ Client-side navigation → notFoundComponent renders correctly
  • ❌ Direct URL access → Shows "Cannot GET /path"
  • ❌ Page refresh → Shows "Cannot GET /path"

Example:

export const Route = createFileRoute("/_authed/agency")({
  loader: async ({ context }) => {
    const userAccess = await getUserAccess();
    if (!isAgencyMember(userAccess)) {
      throw notFound();
    }
    return { user: context.user, userAccess };
  },
  component: AgencyLayout,
});

Accessing /agency directly shows "Cannot GET /agency" instead of the configured notFoundComponent. Same happens for any non-existent route like /test-route.

Andresuito avatar Nov 24 '25 20:11 Andresuito

I was able to "solve" the issue @Andresuito and I am seeing by removing the nitro() plugin from vite.config.ts. I don't nearly understand enough of what's going here to call this a solution, but hopefully this bit of information can be helpful.

denniskorbginski avatar Dec 11 '25 13:12 denniskorbginski

I was able to "solve" the issue @Andresuito and I am seeing by removing the nitro() plugin from vite.config.ts. I don't nearly understand enough of what's going here to call this a solution, but hopefully this bit of information can be helpful.

isnt nitro required to deploy on vercel?

MattPua avatar Dec 11 '25 14:12 MattPua