React Router v8 on Workers
這是全系列時效性最強的一篇。
React Router v8 在 2026-06-17 GA。它移除了 AppLoadContext,而 AppLoadContext 正是 2025 年每一篇 Cloudflare + React Router 教學存取 binding 的方式 —— 包含 Cloudflare 官方的 framework guide,以及官方的 deploy button 範本。
這不是型別警告。實測結果是每一個請求都回 HTTP 500。
本章要做的第一件事,就是把這個 500 跑出來給你看。
24.1 先看那個 500
Section titled “24.1 先看那個 500”Cloudflare 官方 React Router 指南(頁面顯示最後更新 2026-06-19,也就是 v8 GA 之後兩天)現在仍然這樣寫:
// workers/app.ts —— 官方文件現行內容,在 v8 上會壞export default { async fetch(request, env, ctx) { return requestHandler(request, { cloudflare: { env, ctx }, }); },} satisfies ExportedHandler<CloudflareEnvironment>;// app/routes/home.tsx —— 官方文件現行內容,在 v8 上會壞export function loader({ context }: Route.LoaderArgs) { return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE };}範例專案裡我在 Worker 入口留了一條探針路由,故意把那個普通物件傳下去:
if (url.pathname === "/__probe/plain-object") { const res = await requestHandler( new Request(new URL("/", url), request), { cloudflare: { env, ctx } } as unknown as RouterContextProvider, ); return Response.json({ status: res.status, body: (await res.text()).slice(0, 200) });}實測輸出:
{ "status": 500, "body": "Unexpected Server Error\n\nError: Invalid `context` value provided to `handleRequest`. You must return an instance of `RouterContextProvider` from your `getLoadContext` function."}這段檢查在 react-router@8.3.0 的 dist/development/lib/server-runtime/server.js 裡是硬編碼的:
if (initialContext && !(initialContext instanceof RouterContextProvider)) { let error = new Error("Invalid `context` value provided to `handleRequest`. ..."); handleError(error); return returnLastResortErrorResponse(error, serverMode);}AppLoadContext 這個字串在 react-router@8.3.0 的整個 dist/ 裡出現 0 次。不是 deprecated,不是有 shim —— 是徹底移除。
本章存在的理由: React Router 官方的部署頁把 Cloudflare 的部分指向 Cloudflare 文件;Cloudflare 文件展示的是 v8 會拒絕的 v7 程式碼;Cloudflare 的 deploy button 範本目前還停在 React Router 7.9.6。而唯一正確的來源 ——
npm create cloudflare產生的程式碼 —— 沒有任何一頁文字說明它為什麼那樣寫。三個官方來源、三個不同答案,全都從同一頁連得到。
24.2 版本基準
Section titled “24.2 版本基準”實測 npm registry:
| 套件 | 版本 | 備註 |
|---|---|---|
react-router | 8.3.0(2026-07-22) | v8.0.0 GA 2026-06-17 |
@react-router/dev | 8.3.0 | peer: vite: ^7 || ^8、wrangler: ^4(optional) |
@cloudflare/vite-plugin | 1.50.0 | |
vite | 8.2.0 | |
react / react-dom | 19.2.8 | |
react-router-dom | 7.18.2 | 沒有 8.x,這個套件已被移除 |
react-router@8.3.0 的 package.json 實測:
{ "type": "module", "engines": { "node": ">=22.22.0" }, "peerDependencies": { "react": ">=19.2.7", "react-dom": ">=19.2.7" }, "exports": { ".": ..., "./dom": ..., "./internal": ..., "./internal/react-server-client": ... }}沒有 require 條件 —— v8 是 ESM-only。 這一點在 Workers 上有實際後果,24.9 會談。
Node 最低 22.22.0。Vite 最低 7(v8 的 changelog:「Require Vite 7+ and make the Vite Environment API build path mandatory」)。
⚠️ React Router 自己的升級指南在同一頁上自相矛盾:
future.v8_viteEnvironmentApi那一節還寫著「This is only available when using Vite 6+」,而三個畫面之上的 Minimum Versions 要求 Vite 7+。那句話是 v7 時代留下來的。
24.3 v8 存取 binding 的正確方式:完全不用 context
Section titled “24.3 v8 存取 binding 的正確方式:完全不用 context”npm create cloudflare@latest -- --framework=react-router 現在產出的 loader 長這樣 —— 注意它根本沒有 context 參數:
import { env } from "cloudflare:workers";import type { Route } from "./+types/home";
export function loader() { return { message: env.VALUE_FROM_CLOUDFLARE };}Worker 入口也只有一行:
import { createRequestHandler } from "react-router";
const requestHandler = createRequestHandler( () => import("virtual:react-router/server-build"), import.meta.env.MODE,);
export default { async fetch(request) { return requestHandler(request); },} satisfies ExportedHandler<Env>;requestHandler 的第二個參數不傳。沒有 declare module "react-router",沒有 getLoadContext。
範例專案的 app/routes/home.tsx 用同樣的方式讀 D1,實測輸出:
{ "message": "hello from wrangler.jsonc", "d1": { "one": 1 }, "bindingsSeenFromModuleImport": ["DB", "VALUE_FROM_CLOUDFLARE"]}D1 查詢在 loader 裡正常執行。這件事之所以成立,回到第 3 章的規則:
官方 bindings 文件:「Workers 不允許在請求 context 之外做 I/O。這代表即使
env在頂層 scope 可存取,你也無法呼叫每一個 binding 的方法。」
loader 就在請求 context 裡,所以沒問題。但同一份 env 如果在模組頂層拿來呼叫 env.DB.prepare(...),會炸 —— 這正是第 1 章那個 crypto.randomUUID() 在 global scope 失敗的同一條規則。
這裡有一個必須誠實說明的地方: 沒有任何一頁官方文字說「v8 請改用 cloudflare:workers」。這個建議只存在於官方產生器產出的程式碼裡。所以本章的說法是「這是官方 CLI 產生的寫法」,而不是「文件這樣說」。
24.4 需要 ctx.waitUntil 或 request.cf 時
Section titled “24.4 需要 ctx.waitUntil 或 request.cf 時”import { env } 給不了兩樣東西:ExecutionContext 和 request.cf。這兩個都是 per-request 的,模組層的 env 本質上拿不到。
需要它們時,才需要建立 context。v8 的正確做法是三步:
import { createContext, createRequestHandler, RouterContextProvider } from "react-router";
// 1. createContext 建立的是「有型別的鍵」,不是 provider 本身export const cfContext = createContext<{ ctx: ExecutionContext; cf: IncomingRequestCfProperties | undefined;}>();
const requestHandler = createRequestHandler( () => import("virtual:react-router/server-build"), import.meta.env.MODE,);
export default { async fetch(request, env, ctx) { // 2. provider 用 new 建立 const context = new RouterContextProvider(); // 3. set 進去 context.set(cfContext, { ctx, cf: request.cf }); return requestHandler(request, context); },} satisfies ExportedHandler<Env>;export async function loader({ context }: Route.LoaderArgs) { const cf = context.get(cfContext); cf.ctx.waitUntil(logSomething()); return { colo: cf.cf?.colo ?? null };}實測 /probe 的輸出:
{ "contextIsProvider": "RouterContextProvider", "hasWaitUntil": true, "colo": "DFW", "envKeys": ["DB", "VALUE_FROM_CLOUDFLARE"]}request.cf 在本地 wrangler dev 也有值(colo: "DFW")。
兩個容易講錯的細節:
createRequestHandler本身不吃 context。 它的簽章是(build, mode?)。拒絕發生在它回傳的那個 handler 上:type RequestHandler = (request: Request, loadContext?: RouterContextProvider) => Promise<Response>;RouterContextProvider不是「用createContext()建出來的」。createContext<T>()產生的是一個型別化的 key;provider 用new RouterContextProvider()建立,兩者是不同的東西。
什麼時候用哪一種
Section titled “什麼時候用哪一種”| 需求 | 做法 |
|---|---|
| 讀 KV / D1 / R2 / 環境變數 | import { env } from "cloudflare:workers" |
ctx.waitUntil() | createContext + RouterContextProvider |
request.cf(國家、colo、TLS 資訊) | 同上,或直接在 route 裡用 request |
| 測試時要注入假的 binding | context(依賴注入比模組 import 好測) |
官方 bindings 文件自己也提了這個取捨:「雖然從 cloudflare:workers 用 env 比一層層傳遞簡單,但把 env 當參數傳是一個對依賴注入與測試有幫助的模式。」第 38 章講測試時會回到這一點。
24.5 Middleware:v8 起永遠開啟
Section titled “24.5 Middleware:v8 起永遠開啟”v8 移除了 future.v8_middleware 旗標,middleware 現在是預設功能。實測 react-router@8.3.0 的 runtime export 裡沒有任何名稱含 middleware 的值 —— MiddlewareFunction 是純型別 export,所以只出現在 .d.ts 裡。沒有 unstable_ 前綴。
import { createContext } from "react-router";import type { Route } from "./+types/probe";
export const beforeNext = createContext<string>("never-set");export const afterNext = createContext<string>("never-set");
const timing: Route.MiddlewareFunction = async ({ context }, next) => { const started = Date.now(); context.set(beforeNext, "set-before-next"); // loader 看得到 const res = await next(); context.set(afterNext, "set-after-next"); // loader 看不到,太晚了 res.headers.set("x-ch24-middleware-ms", String(Date.now() - started)); return res;};
export const middleware: Route.MiddlewareFunction[] = [timing];
export async function loader({ context }: Route.LoaderArgs) { return { seenBeforeNext: context.get(beforeNext), seenAfterNext: context.get(afterNext), };}實測:
{ "seenBeforeNext": "set-before-next", "seenAfterNext": "never-set"}回應標頭:x-ch24-middleware-ms: 4。
await next() 是分界線。 之前的程式碼在 loader 之前跑,之後的在 loader 之後跑。這是 onion model,和你熟悉的 Hono middleware(第 5 章)語意一致。所以「認證」要寫在 next() 之前,「加標頭 / 記錄耗時」寫在之後。
middleware 與 loader 共用同一個 RouterContextProvider 實例,這就是 context 的主要用途:middleware set,loader get。
執行順序是 parent → child 往下,child → parent 往上。route module 有兩個 export:middleware(server)與 clientMiddleware(client)。
官方文件有一句對 Workers 特別重要的說明:
「Document request 不論有沒有 loader 都會執行 server middleware,因為我們仍然在一個 handler 裡渲染 UI。Client-side navigation 只有在向伺服器發出
.data請求(因為有 action/loader)時才會執行 server middleware。」
24.6 v8 改變的兩件事會影響你的 Worker 層
Section titled “24.6 v8 改變的兩件事會影響你的 Worker 層”這兩點在 React Router 的 changelog 裡,但沒有任何 Cloudflare 文件提到,而它們直接影響你在 Worker 裡寫的路由與快取邏輯。
(一)request.url 現在是原始 URL,含 .data 後綴
Section titled “(一)request.url 現在是原始 URL,含 .data 後綴”future.v8_passThroughRequests 成為預設。實測,對 /probe.data 發請求,loader 裡的 request.url 是:
http://localhost:9026/probe.data不是 http://localhost:9026/probe。
任何在 Worker 層用 request.url 做 pattern matching 的邏輯都要重新檢查 —— 快取鍵、A/B 分流、租戶解析。v8 給了 loader 一個新的 url 參數放正規化後的 URL,route 內部請用它。
(二)root data 路徑改名
Section titled “(二)root data 路徑改名”實測:
GET /_root.data -> 404GET /_.data -> 200v8 把 root data 請求從 /_root.data 改成 /_.data,並新增 /path/_.data 形式。如果你在 Cloudflare 上設了任何比對 .data 的 Cache Rule、WAF 規則,或第 7 章的 _routes.json / run_worker_first glob,全部要更新。
24.7 Vite 設定與建置輸出
Section titled “24.7 Vite 設定與建置輸出”vite.config.ts
Section titled “vite.config.ts”import { reactRouter } from "@react-router/dev/vite";import { cloudflare } from "@cloudflare/vite-plugin";import { defineConfig } from "vite";
export default defineConfig({ plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), reactRouter(), ], resolve: { tsconfigPaths: true },});viteEnvironment: { name: "ssr" } 是官方明確為 v8 記載的:
「如果你把 Cloudflare Vite plugin 與 TanStack Start 或 React Router v8 一起使用,你的 Worker 會被用於 server-side rendering 並與框架緊密整合。為此你應該透過
viteEnvironment.name把它指派到ssr環境…這會把 Worker 的環境設定與框架的 SSR 設定合併,並確保 Worker 被納入框架的建置輸出。」
cloudflare() 放在 reactRouter() 之前。所有官方範例都這樣寫,但沒有任何文件說這是必須的順序 —— 本章只陳述慣例。
兩個 2025 年教學會寫錯的地方:
vite-tsconfig-paths已經不需要了,改用 Vite 內建的resolve: { tsconfigPaths: true }。react-router.config.ts裡不再有future旗標區塊。 實測@react-router/dev@8.3.0的FutureConfig只剩unstable_enableNodeReadableStream與unstable_optimizeDeps,沒有任何v8_*旗標。預設範本的內容就只有{ ssr: true }。
不要手寫 assets 區塊
Section titled “不要手寫 assets 區塊”官方 static assets 文件:
「執行
vite build時,會產生一個wrangler.json設定檔作為建置輸出的一部分。這個檔案裡的assets.directory欄位會自動填入你的client建置輸出路徑。因此不需要在你的輸入 Worker 設定裡提供assets.directory欄位。」
實測 npx react-router build 之後,build/server/wrangler.json 的關鍵欄位:
{ "name": "ch24-react-router", "main": "index.js", "assets": { "directory": "../client" }, "no_bundle": true, "rules": [{ "type": "ESModule", "globs": ["**/*.js", "**/*.mjs"] }], "compatibility_date": "2026-07-24", "compatibility_flags": ["nodejs_compat"], "d1_databases": [{ "binding": "DB", ... }], "vars": { "VALUE_FROM_CLOUDFLARE": "hello from wrangler.jsonc" }}輸出結構:
build/ client/ .assetsignore <- 自動產生,內容是 "wrangler.json" 與 ".dev.vars" assets/... server/ index.js <- 你的 Worker wrangler.json <- 自動產生的部署設定 assets/....wrangler/ deploy/config.json <- {"configPath":"../../build/server/wrangler.json"}三件事值得單獨記住:
main有兩個不同的值,這不是矛盾。 你手寫的wrangler.jsonc裡main指向 TypeScript 原始碼(./workers/app.ts);產生的build/server/wrangler.json裡main是建置後的 chunk(index.js)。no_bundle: true。 Vite 已經打包過,Wrangler 不可以再打包一次。.wrangler/deploy/config.json是關鍵。 它讓wrangler deploy自動找到產生的設定檔。所以部署時不要加-c wrangler.jsonc—— 那是輸入設定,main指向.ts,會失敗。
npm run build && npx wrangler deploy24.8 已移除的 API 清單
Section titled “24.8 已移除的 API 清單”| API | 狀態 | 取代 |
|---|---|---|
AppLoadContext | 移除(dist 裡 0 次) | RouterContextProvider |
cloudflareDevProxy() | 移除 | @cloudflare/vite-plugin 的 cloudflare() |
@react-router/dev/vite/cloudflare | 整個 export 移除 | 同上 |
react-router-dom | 套件移除(最後版本 7.18.2) | react-router / react-router/dom |
json() / defer() | 移除(實測 runtime export 裡沒有) | data() 或直接回傳物件 / Response |
future.v8_middleware 旗標 | 移除 | middleware 永遠開啟 |
MiddlewareEnabled 型別 | 移除 | 不再需要 |
meta / useMatches 的 data 欄位 | 移除 | loaderData |
hasErrorBoundary(route 物件) | 移除 | 內部欄位 |
升級指南裡的 vite.config 遷移 diff:
import { reactRouter } from "@react-router/dev/vite";import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";import { cloudflare } from "@cloudflare/vite-plugin";
export default defineConfig({ plugins: [ cloudflareDevProxy(), cloudflare(), reactRouter(), ],});
json()/defer()有一個文件陷阱: 它們是在 v7 的版本線裡被移除的,所以 v8 的升級指南與 changelog 完全沒有提到它們。從 v6 直接跳到 v8 的人不會收到任何警告,只會看到json is not exported。
一個很容易踩的套件陷阱
Section titled “一個很容易踩的套件陷阱”@react-router/cloudflare@8.3.0 仍然存在、仍然在發布 —— 但它是 Cloudflare Pages Functions 的 adapter,不是 Workers 的。
它 export 的是 createWorkersKVSessionStorage 與一個型別為 PagesFunction<Env>、使用 EventContext 的 createRequestHandler。Workers 這條路徑完全不會用到它,C3 範本的 dependencies 裡也沒有它。
如果有教學叫你 npm i @react-router/cloudflare 來做 Workers 應用,那是錯的。
24.9 Cloudflare 上的已知限制
Section titled “24.9 Cloudflare 上的已知限制”SPA mode 與 prerendering 不支援
Section titled “SPA mode 與 prerendering 不支援”官方 React Router 指南原文:
「使用 Cloudflare Vite plugin 時,目前不支援 SPA mode 與 prerendering。如果你想在 SPA 中使用 React Router,我們建議從 React 範本開始,並把 React Router 當成函式庫使用。」
不過這句話正在鬆動:@cloudflare/vite-plugin 已經有 experimental.prerenderWorker 選項(「這是實驗性功能,可能隨時改變或移除」)。所以這是「目前不支援的組態」,不是永久限制。
ESM-only 造成的相容性問題
Section titled “ESM-only 造成的相容性問題”v8 是 ESM-only,而 workerd 也強制 ESM。這對只發 CJS 的第三方套件(MUI 是最常見的例子)是個問題。
有一個到 2026-08-01 為止仍然開啟的 issue(workers-sdk#14555,2026-07-05 開啟)描述了一個很惡毒的連鎖反應:CJS 套件會拋 require is not defined;用 noExternal: true 繞過,會把 React Router 拉進 Vite 的 dep cache,造成兩份 React Router 實例,於是 24.1 那個 instanceof RouterContextProvider 檢查失敗,又回到 500。
這一點沒有出現在任何官方文件裡。 如果你的 dashboard 打算引入大型 CJS UI 套件,先確認它有沒有 ESM 版本。
nodejs_compat
Section titled “nodejs_compat”C3 產生的專案一定會帶 compatibility_flags: ["nodejs_compat"] —— 這是 C3 的 addNodejsCompatFlag() 對所有 JS/TS 專案無條件加上的,不是 React Router 特有的需求。
沒有任何官方文件說 React Router v8 需要 nodejs_compat。 本章的範例保留了它(與範本一致),但這是「範本這樣做,關掉未經測試」,不是「必須」。
compatibility_date 沒有標準答案
Section titled “compatibility_date 沒有標準答案”- C3:scaffold 當天的日期(實測
getWorkerdCompatibilityDate()就是formatCompatibilityDate(new Date()),儘管 spinner 文字寫著「Retrieving current workerd compatibility date」,並沒有向 workerd 版本對齊)。 - Wrangler autoconfig:文件範本寫
"$today"。 - 過時的官方 starter template:釘在
"2025-10-08"。
複製 starter template 的人會拿到一個十個月前的 runtime baseline。第 2 章講過 compatibility_date 是 runtime 的版本鎖 —— 這不是小事。
24.10 LinkForge:apps/dashboard
Section titled “24.10 LinkForge:apps/dashboard”把前面各章的 binding 接上來。目錄結構(第 26 章會把它變成正式的 monorepo):
apps/dashboard/ app/ root.tsx routes.ts routes/ _index.tsx 儀表板總覽 links.tsx 短網址列表(loader 讀 D1) links.$slug.tsx 單一短網址 + 點擊分析(讀 Analytics Engine) api.reports.$.tsx resource route,代理報表查詢 context.ts 共用的 context keys workers/app.ts wrangler.jsoncapp/context.ts 把所有 context key 集中在一處,避免 route 之間互相 import 造成循環:
import { createContext } from "react-router";
export const cfContext = createContext<{ ctx: ExecutionContext; cf: IncomingRequestCfProperties | undefined;}>();
export const sessionContext = createContext<{ tenantId: string; userId: string } | null>(null);根 route 的 middleware 做認證,把 session 放進 context:
import { redirect } from "react-router";import { env } from "cloudflare:workers";import { sessionContext } from "./context";import type { Route } from "./+types/root";
const auth: Route.MiddlewareFunction = async ({ request, context }, next) => { const session = await readSession(env.KV, request); if (!session && new URL(request.url).pathname.startsWith("/app")) { throw redirect("/login"); } context.set(sessionContext, session); // 一定要在 next() 之前 return next();};
export const middleware: Route.MiddlewareFunction[] = [auth];loader 拿 session 與 binding:
import { env } from "cloudflare:workers";import { sessionContext } from "../context";import type { Route } from "./+types/links";
export async function loader({ context }: Route.LoaderArgs) { const session = context.get(sessionContext); if (!session) throw new Response("Unauthorized", { status: 401 });
const { results } = await env.DB .prepare("select slug, url, created_at from links where tenant_id = ?1 order by created_at desc limit 50") .bind(session.tenantId) .all();
return { links: results };}注意 tenantId 來自 middleware 設定的 session,不是 URL 參數 —— 第 22 章談 SQL 注入時強調過同一件事。
TanStack Query 放在哪裡
Section titled “TanStack Query 放在哪裡”官方沒有任何 React Router v8 + TanStack Query on Workers 的指引。 我確認過三件事:React Router 文件與 changelog 裡 “TanStack” 出現 0 次;Cloudflare 的 React Router 指南沒有提到;Cloudflare 的 TanStack Start 指南是另一個框架,與 React Router 無關。
所以以下是本章的原創建議,不是官方立場:
loader 負責首屏,TanStack Query 負責之後的互動。 兩者的職責不要重疊:
import { useLoaderData } from "react-router";import { useQuery } from "@tanstack/react-query";
export default function Links() { const { links } = useLoaderData<typeof loader>();
// 首屏用 loader 的資料當 initialData,之後的輪詢/失效交給 Query const { data } = useQuery({ queryKey: ["links"], queryFn: () => fetch("/api/links").then((r) => r.json()), initialData: links, staleTime: 30_000, });
return <LinkTable links={data} />;}一個 Workers 特有的地雷:絕對不要在模組層建立 QueryClient。 第 1 章與第 3 章講過 isolate 會在多個請求之間重用,模組層的物件會跨請求共享 —— 一個模組層的 QueryClient 會讓 A 租戶的快取資料洩漏給 B 租戶。伺服器端如果需要 QueryClient,用 createContext<QueryClient>() 加上根 middleware 每個請求建一個新的。
另外,v8 的 splitRouteModules 現在預設為 true,這會改變 clientLoader 落在哪個 chunk。如果你在 clientLoader 裡做 hydration,注意這件事。
24.11 本章實測結論彙整
Section titled “24.11 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | v7 的 requestHandler(request, { cloudflare: {...} }) 在 v8 實測回 HTTP 500,body 為 Invalid \context` value provided to `handleRequest“ | 這是 Cloudflare 官方指南現行展示的程式碼 |
| 2 | AppLoadContext 在 react-router@8.3.0 的 dist 裡出現 0 次 | 完全移除,非 deprecated |
| 3 | 拒絕發生在 createRequestHandler 回傳的 handler 上,不是 createRequestHandler 本身 | 簽章是 (request, loadContext?: RouterContextProvider) |
| 4 | RouterContextProvider 用 new 建立;createContext() 產生的是型別化的 key | 兩者是不同的東西 |
| 5 | import { env } from "cloudflare:workers" 在 loader 裡實測可正常查 D1 | 官方 CLI 產出的寫法,但無任何文字文件背書 |
| 6 | request.cf 在本地 wrangler dev 有值(實測 colo: "DFW") | — |
| 7 | middleware 中 next() 之前的 context.set loader 看得到,之後的看不到 | 實測 set-before-next / never-set |
| 8 | runtime export 裡沒有任何含 middleware 的值 | MiddlewareFunction 是純型別 export |
| 9 | .data 請求的 request.url 實測含 .data 後綴 | v8_passThroughRequests 成為預設,Worker 層路由要檢查 |
| 10 | /_root.data 實測 404,/_.data 實測 200 | Cache Rule / WAF / _routes.json 都要更新 |
| 11 | vite build 產生 build/server/wrangler.json,main: "index.js"、assets.directory: "../client"、no_bundle: true | 不要手寫 assets 區塊 |
| 12 | .assetsignore 自動產生,內容是 wrangler.json 與 .dev.vars | 避免把設定檔上傳成靜態資源 |
| 13 | .wrangler/deploy/config.json 指向產生的設定 | 部署時不要加 -c wrangler.jsonc |
| 14 | react-router@8.3.0 是 ESM-only(exports 無 require 條件) | CJS 相依套件會出問題 |
| 15 | json() / defer() 實測不在 runtime export 裡,且 v8 升級文件完全沒提 | 從 v6 跳 v8 的人不會收到警告 |
| 16 | react-router-dom 最後版本 7.18.2,沒有 8.x | 套件已移除 |
| 17 | @react-router/cloudflare@8.3.0 仍存在,但是 Pages Functions adapter | Workers 專案不該安裝它 |
| 18 | @react-router/dev@8.3.0 的 FutureConfig 沒有任何 v8_* 旗標 | react-router.config.ts 只要 { ssr: true } |
| 19 | Node ≥ 22.22.0、React ≥ 19.2.7、Vite ≥ 7(實測範本用 Vite 8.2.0) | Vite 6 不再相容 |
| 20 | 官方 starter template 仍是 React Router 7.9.6,compatibility_date 釘在 2025-10-08 | deploy button 與 CLI 產出互不相容 |
24.12 動手練習
Section titled “24.12 動手練習”- 把範例的
/__probe/plain-object路由拿掉as unknown as RouterContextProvider,看 TypeScript 在編譯期就攔下來 —— 然後想想為什麼官方文件的程式碼在 v7 型別下不會報錯。 - 在
middleware裡next()之前與之後各設一個 context,用.data請求(curl /probe.data)觀察與 document request 的差異。 - 寫一個 Worker 層的租戶解析邏輯,先用
request.url做 pattern matching,然後對/x.data發請求,觀察它如何壞掉;再改用 loader 的url參數修正。 - 把 LinkForge dashboard 的 session 從 middleware 改成
import { env }直接讀 KV,比較兩種寫法在第 38 章的測試裡各自有多好寫。
- React Router upgrade guide · CHANGELOG · Middleware how-to · Deploying
- Cloudflare React Router 指南(⚠️ 展示的是 v8 會拒絕的 v7 程式碼)
- Vite plugin — Vite Environments · Static Assets · Automatic configuration
- Workers Bindings (env) · Workers TypeScript
- workers-sdk#14555 — vite-plugin + RR v8 CJS / dual-package hazard(仍開啟)
- 版本與 API 來源:npm registry 中繼資料,以及
node_modules/react-router@8.3.0的實際dist/
下一章(第 25 章):Astro on Workers。同樣有大版本斷層 ——
@astrojs/cloudflarev14 移除了Astro.locals.runtime.*,而npm create cloudflare -- --framework=astro目前產出的範本仍然塞入已不存在的platformProxy。