From 7c56295d5a0e3e89d0ddefdef9859638ea1fd0b2 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 27 Mar 2026 20:52:47 -0500 Subject: [PATCH 1/2] fix: don't await createFromReadableStream before hydrateRoot (#695) 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. --- .../vinext/src/server/app-browser-entry.ts | 2 +- .../e2e/cloudflare-workers/hydration.spec.ts | 13 ++++++++++ .../cf-app-basic/app/effect-test/page.tsx | 26 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/cf-app-basic/app/effect-test/page.tsx diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts index 3713228b1..da579840f 100644 --- a/packages/vinext/src/server/app-browser-entry.ts +++ b/packages/vinext/src/server/app-browser-entry.ts @@ -179,7 +179,7 @@ async function main(): Promise { registerServerActionCallback(); const rscStream = await readInitialRscStream(); - const root = await createFromReadableStream(rscStream); + const root = createFromReadableStream(rscStream); reactRoot = hydrateRoot( document, diff --git a/tests/e2e/cloudflare-workers/hydration.spec.ts b/tests/e2e/cloudflare-workers/hydration.spec.ts index ed92bb98e..73b37f3ee 100644 --- a/tests/e2e/cloudflare-workers/hydration.spec.ts +++ b/tests/e2e/cloudflare-workers/hydration.spec.ts @@ -22,6 +22,19 @@ test.describe("Cloudflare Workers Hydration", () => { void consoleErrors; }); + // Regression test for https://github.com/cloudflare/vinext/issues/695 + // createFromReadableStream was awaited before hydrateRoot, blocking effects. + test("useEffect fires after RSC hydration", async ({ page, consoleErrors }) => { + await page.goto(`${BASE}/effect-test`); + + // useEffect should fire and update the status from "effect-pending" to "effect-fired" + await expect(page.locator('[data-testid="effect-status"]')).toHaveText("effect-fired", { + timeout: 10_000, + }); + + void consoleErrors; + }); + test("page with timestamp hydrates without mismatch", async ({ page, consoleErrors }) => { // This test specifically verifies the fix for GitHub issue #61. // The home page has a timestamp that would cause hydration mismatch diff --git a/tests/fixtures/cf-app-basic/app/effect-test/page.tsx b/tests/fixtures/cf-app-basic/app/effect-test/page.tsx new file mode 100644 index 000000000..c9a7d198f --- /dev/null +++ b/tests/fixtures/cf-app-basic/app/effect-test/page.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useEffect, useState } from "react"; + +// Regression test for https://github.com/cloudflare/vinext/issues/695 +// useEffect callbacks were never firing after RSC hydration because +// createFromReadableStream was awaited before being passed to hydrateRoot, +// which blocked hydration until the entire RSC stream was consumed. +export default function EffectTestPage() { + const [fired, setFired] = useState(false); + const [count, setCount] = useState(0); + + useEffect(() => { + setFired(true); + }, []); + + return ( +
+

{fired ? "effect-fired" : "effect-pending"}

+

Count: {count}

+ +
+ ); +} From 96e238eb6073c0608f11bfffd2112ecf7748dfed Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 27 Mar 2026 21:01:42 -0500 Subject: [PATCH 2/2] fix: move effect-test page to app-router-cloudflare example 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. --- .../app-router-cloudflare}/app/effect-test/page.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests/fixtures/cf-app-basic => examples/app-router-cloudflare}/app/effect-test/page.tsx (100%) diff --git a/tests/fixtures/cf-app-basic/app/effect-test/page.tsx b/examples/app-router-cloudflare/app/effect-test/page.tsx similarity index 100% rename from tests/fixtures/cf-app-basic/app/effect-test/page.tsx rename to examples/app-router-cloudflare/app/effect-test/page.tsx