Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン
#8,783 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
react, typescript
領域
backend, frontend

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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.
主要言語
TypeScript
スター
15.2k
フォーク
1.9k
平均マージ
1日 13時間
マージ済み PR(30日)
76

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

TanStack/router のほかの issue

TanStack/router の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。