Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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

Aperta
#8,783 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
react, typescript
Ambito
backend, frontend

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.
Lingua principale
TypeScript
Stelle
15.2k
Fork
1.9k
Merge medio
1g 13h
PR unite (30g)
76

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di TanStack/router

Tutte le issue di TanStack/router

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.