fix: don't await createFromReadableStream before hydrateRoot (#695)#700
Conversation
commit: |
|
|
/bigbonk review please |
There was a problem hiding this comment.
Review
Core fix: correct and well-motivated
The one-line fix at app-browser-entry.ts:182 is correct. createFromReadableStream returns a React thenable, not a resolved component tree. The RSC contract requires passing this thenable directly to hydrateRoot, which suspends on it internally. Awaiting it first blocks the entire hydration pipeline until the RSC stream finishes consuming, which prevents React from ever scheduling passive effects (useEffect).
This matches the pattern already used in app-ssr-entry.ts:174:
flightRoot = createFromReadableStream(ssrStream); // no awaitThe regression test (e2e + fixture page) is well-targeted: a "use client" component with useEffect(() => setFired(true), []) that flips a data-testid from "effect-pending" to "effect-fired".
Refactor: clean extraction following AGENTS.md guidance
The bulk of the diff extracts inline generated code from pages-server-entry.ts into typed helper modules (pages-node-compat.ts, pages-api-route.ts), which is exactly what AGENTS.md recommends:
generated code may describe route-specific imports, manifests, and thin closures, but it should not own substantial request lifecycle logic
Positive details:
- ISR cache helpers now delegate to the shared
isr-cache.tsinstead of duplicatingfnv1a64,pendingRegenerations, andgetCacheHandler()calls inline - Uses the existing
addQueryParamutility fromutils/query.tsinstead of reimplementing the duplicate-key promotion logic Buffer→new Uint8Array(buffer)insend()is more portable to Workerspages-api-route.test.tshas solid coverage: query merging, JSON parse errors, urlencoded bodies, Buffer sends, error reporting, body size limits
Minor nits (non-blocking)
-
Fragile string-based error matching (
pages-api-route.ts:76):(error as Error).message === "Request body too large"is brittle. A dedicated error class (likePagesApiBodyParseError) would be more robust. This matches the pre-existing behavior though, so not a blocker. -
parsePagesApiBodydiscards stack trace (pages-node-compat.ts:108-109): The catch re-throwsnew Error("Request body too large")instead of re-throwing the original error. Since this is a fresh extraction, it'd be a good time to preserve the original error, but again non-blocking. -
Thin ISR wrappers in the generated entry are technically redundant indirection (
function isrGet(key) { return __sharedIsrGet(key); }), but I assume they're intentional to minimize changes to existing call sites within the template. Fine as-is.
LGTM -- the fix is clearly correct, the regression test covers the exact failure mode, and the refactor improves maintainability.
|
Review posted and approved on PR #700. Here's a summary of the review: Verdict: APPROVED The PR has two parts:
Three non-blocking nits noted in the review body:
|
createFromReadableStream returns a React thenable, not a resolved tree. Awaiting it blocked hydrateRoot until the entire RSC stream was consumed, which prevented useEffect callbacks from ever firing in production on Cloudflare Workers where RSC chunks arrive progressively via script tags. Pass the thenable directly to hydrateRoot so React resolves components lazily as stream chunks arrive -- matching the RSC contract and the pattern already used in the SSR entry. Adds a regression test fixture and e2e test for the Workers hydration project that verifies useEffect fires after RSC hydration.
The cloudflare-workers e2e project builds examples/app-router-cloudflare, not tests/fixtures/cf-app-basic. Move the regression test fixture page to the correct location so the /effect-test route is actually served.
0b82f1f to
96e238e
Compare
Summary
Fixes #695 --
useEffectcallbacks never firing after RSC hydration on Cloudflare Workers.createFromReadableStreamreturns a React thenable, not a resolved component tree. Awaiting it before passing tohydrateRootblocked hydration until the entire RSC stream was consumed, which prevented passive effects from ever being scheduled.<script>tags, so the stream takes longer to close -- making the bug reliably reproducible there but not locally (where RSC data is available synchronously inwindow.__VINEXT_RSC__).awaitso the thenable is passed directly tohydrateRoot, matching the React RSC contract and the pattern already used inapp-ssr-entry.ts.Changes
packages/vinext/src/server/app-browser-entry.ts-- removeawaitoncreateFromReadableStream(line 182)tests/fixtures/cf-app-basic/app/effect-test/page.tsx-- new"use client"fixture page that usesuseEffectto flip a status flagtests/e2e/cloudflare-workers/hydration.spec.ts-- regression test that verifiesuseEffectfires after RSC hydration on the Workers e2e project