router icon indicating copy to clipboard operation
router copied to clipboard

Not Found in beforeLoad and Loader does not behave as expected

Open ViewableGravy opened this issue 1 year ago • 2 comments

Describe the bug

As described in the steps below, It looks like the notFound error that can be thrown from beforeLoad or the loader is not properly causing the right route to render their notFound component.

If notFound is thrown in beforeLoad, the root route will ALWAYS be the one to render the not found component. Based on router dev tools (which I have inserted above the root route so they do not unmount in my example), it looks like this is because throwing a not found error in beforeLoad causes all parent routes to also become notFound and therefore the root handles it, meaning that no layout is rendered.

If notFound is thrown in a loader, assuming that the loader ran as a result of a navigation or preload, then the notFound behaviour works as expected. However, if the loader is running on the first render of the router, this error propagates to the root route and causes the entire page to show the not found component. Interestingly, in this case, the about route doesn't actually get marked as notFound in the router state, but for some reason the root route still handles the notFound instead of the about component.

image

Your Example Website or App

https://stackblitz.com/edit/tanstack-router-cxbsxj?file=src%2Froutes%2F__root.tsx

Steps to Reproduce the Bug or Issue

  1. Load app to index route and navigate to before-load-not-found route. The root route will handle the not found instead of about, despite about being a parent and rendering an Outlet ❌
  2. Load app to index route and navigate to loader-not-found route. The about route renders it's layout and a not found in the outlet position ✅
  3. Load the app to the loader-not-found route (navigate to that page and then refresh the page). The root route handles the not found ❌

Expected behavior

  1. Load app to index route and navigate to before-load-not-found route. The about route should render it's layout and in the outlet position, the default notFound component
  2. Load app to index route and navigate to loader-not-found route. The about route renders it's layout and a not found in the outlet position
  3. Load the app to the loader-not-found route (navigate to that page and then refresh the page). The about route should render it's layout and in the outlet poisition, the default notFound component

Screenshots or Videos

No response

Platform

  • OS: [Windows]
  • Browser: [Arc]
  • Version: [Tanstack/router: 1.48.1]

Additional context

I noticed this because during my implementation of the router, I've created a middleware function (to run in beforeLoad) to check if a route is enabled for the website our application is running as. Previously this used redirects and as far as I can tell, redirects work completely fine, but may just be luck, because with a redirect, it's not really staying on the same route, so even if the same incorrect matching was to propagate up the routes, it wouldn't really matter since it's going to be redirected anyway.

In my example, I moved the Tanstack Dev tools into the InnerWrap so that they stay mounted on a not-found, helps a little.

I've also tested the following cases which behave the same:

  1. defaultNotFoundComponent manually set (createRouter & RouterProvider)
  2. Explicit not found component on about route
  3. No component set on about route

ViewableGravy avatar Aug 16 '24 09:08 ViewableGravy

Hey, just wondering if there is any update on whether this is something that is getting looked into or aware of.

This bug causes the use of notFound() in beforeLoad and loader to be unreliable and effectively unusable in the context they are designed for.

If there is any other information that I can provide, let me know.

ViewableGravy avatar Sep 03 '24 02:09 ViewableGravy

If notFound is thrown in beforeLoad, the root route will ALWAYS be the one to render the not found component.

From my experience it's even worse: the RootComponent is not rendered at all. This leads to context providers from the RootComponent not being rendered, which then break our NotFound component, as the IntlProvider context is not available for localization.

https://stackblitz.com/edit/github-fcvgqy?file=src%2FContext.ts,src%2Froutes%2F__root.tsx,src%2Fmain.tsx,src%2Froutes%2Fabout.tsx

Additional context: Throwing any other error in beforeLoad works fine, the default error component correctly renders at the closest Outlet.

nstepien avatar Oct 11 '24 19:10 nstepien

This issue hits us hard at the moment. In our use case we want to render "not found" for routes, the current user hast no permission for. So, according to the documentation, we do the authentication and authorization checks in beforeLoad. But throwing a notFount() there, just breaks the application. Because in our case, when rendering the root route, authentication is not done yet (we use different authentication mechanisms for different routes and inject them with router.update to the router context further down the component tree). And also the design system is not initialized in the root route either (also further down). So the whole thing falls back to the ugly root level 404 page too. Instead of keeping the current layout form the layout routes.

Mario-Eis avatar Oct 25 '24 11:10 Mario-Eis

Same, trying to use a catch all on the root and throwing a notFound on the beforeLoad will indeed print out a 404 component without anything from the __root.tsx

nikuscs avatar Oct 29 '24 22:10 nikuscs

We are seeing exactly the same problem in our app: if notFound() is thrown inside beforeLoad, our specified notFoundComponent will never be rendered. This has worked for us before, but I have no information about the router version where it stopped working.

Etsija avatar Dec 30 '24 09:12 Etsija

I can confirm this beforeLoad issue still affects v1.99.3. (Throwing notFound in loader seems to work correctly)

I'm using a workaround using a custom error class inside beforeLoad instead of notFound().

export class NotFoundError extends Error {
  constructor(message?: string) {
    super(message);
    this.name = 'NotFoundError';
    Object.setPrototypeOf(this, NotFoundError.prototype);
  }
}
export const Route = createFileRoute('/about/before-load-not-found')({
  beforeLoad() {
    throw new NotFoundError();
  },
  component: () => <div>Hello /about/not-found!</div>,
  errorComponent: ({ error }) => {
    if (error instanceof NotFoundError) {
      return <>Not Found</>;
    }

    throw error;
  },
});

Demo https://stackblitz.com/edit/tanstack-router-nutwasgj?file=src%2Froutes%2Fabout.before-load-not-found.tsx

longzheng avatar Feb 03 '25 23:02 longzheng

I experienced the same issue.

TLDR - how to fix:

Where you call "createRootRoute" change your code from:

createRootRoute({
  component: () => {
    return <Providers><Outlet /></Providers>
  },
  ...
})

to:

createRootRoute({
  component: () => {
    return <Providers><Outlet /></Providers>
  },
  notFoundComponent: () => {
    return <Providers><Outlet /></Providers>
  }
  ...
})

Explanation:

If you have a route tree with nested routes rootRoute > childRoute > grandchildRoute and you throw an error in the grandchildRoute, the behavior is unexpected.

  • if you throw a standard error ( like throw new Error("") ) in "loader", the defaultErrorComponent from "createRouter" will run
  • if you throw a standard error ( like throw new Error("") ) in "beforeLoad", the defaultErrorComponent from "createRouter" will run
  • if you throw a notFound() error in "loader", the defaultErrorComponent from "createRouter" will run
  • BUT, if you throw a notFound() error in "beforeLoad", the notFoundComponent from "createRootRoute" will run

If i were to guess, there is missing logic that isn't checking the defaultErrorComponent for "notFound" errors that is currently present for standard errors.

On a side note, it seemed strange to me that defaultErrorComponent worked in general because I wouldn't think it should be wrapped with the Providers logic from createRootRoute; but it is. I imagine the logic just overwrites the Outlet with the defaultErrorComponent and then passes this to the rootRoute.

chrisBas avatar Apr 07 '25 19:04 chrisBas

Here is context on why notFound() + beforeLoad() and layouts won't work as expected:

https://discord.com/channels/719702312431386674/1325783958184792086

natedunn avatar Jul 04 '25 16:07 natedunn

As mentioned in the discord discussion this is unfortunately due to how beforeLoad methods are resolved and not something can change at this moment in time.

nlynzaad avatar Oct 16 '25 20:10 nlynzaad

Here is context on why notFound() + beforeLoad() and layouts won't work as expected:

https://discord.com/channels/719702312431386674/1325783958184792086

I dont use discord. Is it possible to add a summary in this thread?

Thanks

leonardo2204 avatar Oct 17 '25 02:10 leonardo2204