Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

ssr:false: redirect thrown in a child beforeLoad during the initial load blanks the app (MatchInner throws `undefined`), reproducer for #7910

Abierto
#8,783 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
48/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
react, typescript
Área
backend, frontend

Línea de trabajo

Start by tracing hydration and the redirected load through MatchInnerImpl, getMatchPromise, CatchBoundary, and loadRouteMatch; the issue gives a reproducible timing scenario but no test file or code locations. Reproduce with the supplied ssr: false parent and child beforeLoad redirect, adding router.invalidate() during the redirected load. Done means the initial page no longer blanks and the redirect reaches the destination reliably.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Versions:

  • @tanstack/react-router 1.170.18
  • @tanstack/router-core 1.171.15
  • @tanstack/react-start 1.167.x (start-client-core 1.170.14)
  • React 19.2
  • Vite 8.1, dev mode

Describe the bug

A full page load of a URL under an ssr: false layout, where a child route's async beforeLoad throws redirect(), can leave the page blank. The console shows Uncaught undefined and "An error occurred in the component".

Navigating to the same URL client-side works.

This is the issue reported in #7910, which was closed for lack of a reproducer. Here is one with no .preload() forwarding.

Minimal reproduction

// routes/zzrepro.tsx
export const Route = createFileRoute("/zzrepro")({
  ssr: false,
  beforeLoad: async () => { await wait(200); return { user: { id: "x" } } },
  component: () => <Outlet />,
})

// routes/zzrepro.a.tsx
export const Route = createFileRoute("/zzrepro/a")({
  beforeLoad: async () => { await wait(200); throw redirect({ to: "/zzrepro/b" }) },
  component: () => <div>A</div>,
})

// routes/zzrepro.b.tsx
export const Route = createFileRoute("/zzrepro/b")({
  beforeLoad: async () => { await wait(200) },
  component: () => <div>B rendered OK</div>,
})

In our app, the redirect alone was enough to blank the page.

In this synthetic version it reproduces deterministically if router.invalidate() is also called while the redirected load is in flight, about 500–900 ms after page load.

For example, Supabase's onAuthStateChange emits SIGNED_IN on every page load. A root-level listener that calls router.invalidate() on it hits this window.

Analysis

  1. The ssr: false match is dehydrated as pending. On the client, hydrate() puts it in the active matches, and router.load() runs the guards.
  2. The child throws a redirect. navigate({ replace: true }) starts a new load whose pending matches reuse the same id for the ssr: false layout. They share _nonReactive.
  3. That load finishes the layout match. loadRouteMatch resolves and clears _nonReactive.loadPromise. This happens before onReady commits the pending matches.
  4. React renders the still-active stale snapshot (status: "pending"). MatchInnerImpl does throw getMatchPromise(match, "loadPromise"), which is undefined.
  5. CatchBoundary stores undefined as the error. That is falsy, so it re-renders its children, which throw again. React gives up and unmounts the root.
  6. The router state afterwards is correct (all matches success), but nothing is mounted. router.state.status stays pending forever.

Workaround we ship (patch-package)

In MatchInnerImpl's getMatchPromise:

return router.getMatch(match.id)?._nonReactive[key]
  ?? match._nonReactive[key]
  ?? router.latestLoadPromise
  ?? new Promise((resolve) => setTimeout(resolve, 0))

Suggested fixes

  • Never throw a non-thenable from the pending branches.
  • Or keep loadPromise alive until the pending matches are committed.
  • Separately, CatchBoundary could treat a thrown undefined as an error, so it renders the errorComponent instead of looping.
Lenguaje dominante
TypeScript
Estrellas
15.2k
Forks
1.9k
Merge medio
1 d 13 h
PR fusionados (30 d)
76

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de TanStack/router

Todos los issues de TanStack/router

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.