diff --git a/examples/app-router-cloudflare/app/effect-test/page.tsx b/examples/app-router-cloudflare/app/effect-test/page.tsx new file mode 100644 index 000000000..c9a7d198f --- /dev/null +++ b/examples/app-router-cloudflare/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}

+ +
+ ); +} 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