/** * The full SSR story, end-to-end (#48, #49): `renderToStream` renders real HTML * through `react-dom/server` and embeds the settled state; `hydrateRoot` + * the ordinary client `mount` then hydrate it with **no mismatch, no refetch, * and the server's state on the first paint** — with zero hydration code in * user land. */ import { act, type ReactNode } from 'react'; import { hydrateRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as Effect from 'effect/Effect'; import * as Layer from 'effect/Layer'; import * as Schema from 'effect/Schema'; import { atom, mount, query, rec } from '../../index.client.ts'; import { renderToStream } from '../../index.ssr.ts'; Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true); const readAll = async (stream: ReadableStream): Promise => { const reader = stream.getReader(); const decoder = new TextDecoder(); let html = ''; for (;;) { const { done, value } = await reader.read(); if (done) { return html + decoder.decode(); } html += decoder.decode(value, { stream: true }); } }; let host: HTMLDivElement; beforeEach(() => { host = document.createElement('div'); document.body.appendChild(host); }); afterEach(() => { host.remove(); for (const script of Array.from(document.querySelectorAll('script[data-effract-state]'))) { script.remove(); } vi.restoreAllMocks(); }); describe('renderToStream → hydrateRoot round-trip (#48, #49)', () => { it('hydrates with no mismatch, no refetch, and the server state on first paint', async () => { const theme = atom.serializable('e2e-theme', 'light', Schema.String); let fetched = 0; const load = Effect.promise(() => { fetched += 1; return Promise.resolve(`greeting-${fetched}`); }); const App = rec(function* () { const t = yield* theme; const g = yield* query(load, 'e2e-greeting'); return (
{t} {g}
); }).suspense(loading); // --- server: mutate state, render, embed -------------------------------- theme.set('dark'); // "server-computed" state the client never computed const stream = await renderToStream(Layer.empty, App, { shell: (app: ReactNode) =>
{app}
, }); const html = await readAll(stream); expect(html).toContain('dark'); expect(html).toContain('greeting-1'); expect(html).toContain('data-effract-state'); expect(fetched).toBe(1); // --- client: a fresh page — DOM from the wire, module state at initials -- theme.set('light'); // simulate the fresh client bundle's initial value host.innerHTML = html; // the state script lands outside the React root const container = host.querySelector('#root') as HTMLElement; // In this test the server and client renderers share one process, so React // dev-warns about the two of them reading the same context — an artifact a // real deployment (separate processes) can never hit. Everything else that // reaches console.error (hydration mismatches above all) fails the test. const errors: unknown[][] = []; vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { if (!String(args[0]).includes('multiple renderers concurrently')) { errors.push(args); } }); const recoverable: unknown[] = []; let root: ReturnType; await act(async () => { root = hydrateRoot(container, mount(Layer.empty, App), { onRecoverableError: (error) => recoverable.push(error), }); }); // (a) no hydration mismatch — nothing recovered, nothing logged; expect(recoverable).toEqual([]); expect(errors).toEqual([]); // (b) the query was not re-fetched on the client; expect(fetched).toBe(1); // (c) the server-mutated atom renders its server value on the first paint. expect(container.querySelector('[data-x="t"]')?.textContent).toBe('dark'); expect(container.querySelector('[data-x="g"]')?.textContent).toBe('greeting-1'); expect(theme.value).toBe('dark'); await act(async () => root.unmount()); }); it('escapes the payload so it cannot break out of the state script', async () => { const payload = ' &\u2028\u2029'; const tricky = atom.serializable('e2e-escape', 'x', Schema.String); tricky.set(payload); const App = rec(function* () { yield* tricky; return
ok
; }); const stream = await renderToStream(Layer.empty, App); const html = await readAll(stream); // Exactly one `` — the state script's own closer; the payload's // is escaped, so the tag cannot be closed early. expect(html.split('').length).toBe(2); expect(html).toContain('\\u003c'); // And the client still decodes the exact original value. tricky.set('x'); host.innerHTML = `
${html.slice(0, html.indexOf('${html.slice(html.indexOf('; await act(async () => { root = hydrateRoot(container, mount(Layer.empty, App)); }); expect(tricky.value).toBe(payload); await act(async () => root.unmount()); }); });