react-router icon indicating copy to clipboard operation
react-router copied to clipboard

[Data Mode] Throwing redirect in middleware isn't short-circuiting route module loading

Open michaelfaith opened this issue 1 month ago • 4 comments

Reproduction

https://codesandbox.io/p/devbox/snowy-resonance-2tmzs3?workspaceId=ws_EuPacS5TJ5ow37kc1WEv3V

Specifically highlighting the console log message coming from the component that should have been redirected away from instead of loading.

Image

System Info

`react-router` 7.9.6

System:
    OS: macOS 15.7.1
    CPU: (12) arm64 Apple M3 Pro
    Memory: 143.67 MB / 36.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.18.0
    Yarn: 1.22.22
    npm: 10.9.3
    pnpm: 10.13.1
  Browsers:
    Chrome: 142.0.7444.162
    Firefox: 145.0
    Safari: 26.1

Used Package Manager

npm

Expected Behavior

From my read of how middleware works, I would expect that if one of the middleware functions in the chain on a route config throws a redirect at some point, that route's module and component shouldn't be loaded or rendered, and the routing should be short-circuited at that point (inferred by the recommendation for using this for Auth). So, I wouldn't expect any attempt to render the original destination for that route, and instead load the route module for the route that was the redirect's destination.

Actual Behavior

What I'm seeing instead, is that the original destination's route module is fully loaded and an initial render happens before the redirect kicks in and moves to the new destination. This presents a potential security risk and some UX concerns.

michaelfaith avatar Nov 19 '25 23:11 michaelfaith

I noticed there was this bug fix that seems somewhat adjacent to this but in reverse for throws that don't originate from redirect. https://github.com/remix-run/react-router/pull/14182

michaelfaith avatar Nov 19 '25 23:11 michaelfaith

This looks like a bug in the logic for determining how deep to render on initial hydration. It's a bit tricky because of the differences between SPA and SSR. The root issue at the moment is that the route doesn't have a loader, and therefore we decide the component is able to be rendered immediately because there's no loader data to wait on.

In a SPA, it does feel like if there's no loader but there is a middleware to run, we shouldn't display the component yet because it could redirect/throw an error, etc. like your example.

But in an SSR app if you had a similar setup with a clientMiddleware we would want to render the component because it would have already been server-rendered in the HTML so we'd get hydration errors if we chose to hold it behind clientMiddleware. The clientLoader.hydrate property sort of avoids this issue in an SSR app because that's the way to trigger client data functions on hydration. So you can't run clientMiddleware without a clientLoader, and thus we don't his this middleware w/o a loader scenario.

I'll look into a fix 👍

For now, the easiest workaround is to throw an empty loader on the route.

brophdawg11 avatar Nov 20 '25 14:11 brophdawg11

Thanks @brophdawg11! Appreciate the quick response 🙏 . Confirmed that adding a (no-op) loader function to our routes as a workaround does work

michaelfaith avatar Nov 20 '25 15:11 michaelfaith

@brophdawg11 any news about this? Adding a noop loader didn't fix it for us

romansndlr avatar Dec 22 '25 14:12 romansndlr