샘플
react-dom 18.3.1: Differentiate React 18 react-dom/server stream controller architecture, renderToString Suspense fallback markup, and Node entrypoint boundary differences
검증된 샘플 — npm react-dom 18.3.1: Differentiate React 18 react-dom/server stream controller architecture, renderToString Suspense fallback markup, and Node…
sha256:b585a75e10818c9c7f8c2aebc3549f5069478f6a50a1a28cfc97be27ff7f8a69
이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다.
통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다.
MIT-0
실행 증거
선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.
- 증거 기준
- 서명된 컨트랙트 통과
- 검증 영수증
- 2
- 빌드한 서명 키
- 2
선언된 환경
node linux x64 node node npm
검증 실행 환경
| 환경 | 컨트랙트 | 단계 | 실행일 |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-17 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
케이스
HOW- 목표
- Differentiate React 18 react-dom/server stream controller architecture, renderToString Suspense fallback markup, and Node entrypoint boundary differences
- 패키지
- 심벌
-
- react-dom/server.renderToPipeableStream
- react-dom/server.renderToString
- react-dom/server.renderToNodeStream
- react-dom/client.createRoot
- react-dom.render
- 환경
- node
- 생성일
- 2026-08-17T02:01:19Z
컨트랙트
- renderToPipeableStream returns a synchronous { pipe, abort } controller object that is neither a Readable stream nor a Promise, where pipe(dest) returns the passed writable destination
- Piping the stream controller into a Writable destination upon onShellReady emits the rendered HTML string synchronously
- renderToString with a suspended component renders fallback HTML with diagnostic template markers instead of throwing or awaiting the promise
- react-dom/server in Node retains deprecated renderToNodeStream but omits renderToReadableStream
- react-dom retains legacy render and hydrate exports whereas modern createRoot and hydrateRoot reside in react-dom/client
파일
- NOTES.md
- csx.json
- package-lock.json
- package.json
- test/contract.mjs
소스
# react-dom v18 Behavior and Entrypoint Contracts
## Prior Search Results
A search for existing solutions returned three related samples:
- `sha256:a8e74b80a92c127ce2b29ee4d261faaea5cb092cc5869188facceec927b1a737`: Focuses on React development vs production runtime contracts across NODE_ENV branching, jsxDEV stubbing, element immutability, and minified error decoding.
- `sha256:059011629c7734f8ac38604635b6d097a2b80c286715c0bbcc0db022a73afa54`: Focuses on runtime breakages from duplicate React instances via peer dependency resolution.
- `sha256:802942a98d62b825180c8f2987341180be283e9fadd47ebdd7ea909279f09ee0`: Focuses on React 19 Node vs Web stream entrypoints in `react-dom/server` and `react-dom/static`.
This contract tests the React 18 server streaming controller architecture, synchronous Suspense fallback emission in `renderToString`, and major version entrypoint differences.
## Key Behaviors Proven
1. **`renderToPipeableStream` Controller Shape**: `renderToPipeableStream` returns a synchronous `{ pipe, abort }` controller object, not a `stream.Readable` instance or a Promise.
2. **Pipe Destination Return**: Calling `controller.pipe(writable)` writes rendered HTML into the writable stream and returns that exact destination instance.
3. **Synchronous Suspense Fallback in `renderToString`**: When encountering a suspended component wrapped in `<Suspense>`, `renderToString` does not throw an error or await a Promise. Instead, it emits the fallback markup inline accompanied by a diagnostic `<template data-msg="...">` comment.
4. **Adjacent Major Entrypoint Differences**:
- `renderToNodeStream` exists in React 18 (deprecated), while `renderToReadableStream` is not exposed on the Node `react-dom/server` entrypoint.
- Legacy `render`, `hydrate`, and `unmountComponentAtNode` remain available on `react-dom`, while `createRoot` and `hydrateRoot` are exported from `react-dom/client`.
## Failure Mode Across Versions
- **React 17**: Fails loudly with `TypeError: renderToPipeableStream is not a function` because Node streaming SSR was introduced in React 18.
- **React 19**: Fails loudly with `TypeError: renderToNodeStream is not a function` and missing legacy root exports on `react-dom`.
{"case":{"believed":"renderToPipeableStream returns a Node.js Readable stream or a Promise resolving to one, while renderToString throws when encountering suspended components","caseId":"case:sha256:8f5e3bbc7ddb6f5272205aa698815da2f95d6b884034a9e011b7fe826b394486","contract":["renderToPipeableStream returns a synchronous { pipe, abort } controller object that is neither a Readable stream nor a Promise, where pipe(dest) returns the passed writable destination","Piping the stream controller into a Writable destination upon onShellReady emits the rendered HTML string synchronously","renderToString with a suspended component renders fallback HTML with diagnostic template markers instead of throwing or awaiting the promise","react-dom/server in Node retains deprecated renderToNodeStream but omits renderToReadableStream","react-dom retains legacy render and hydrate exports whereas modern createRoot and hydrateRoot reside in react-dom/client"],"goal":"Differentiate React 18 react-dom/server stream controller architecture, renderToString Suspense fallback markup, and Node entrypoint boundary differences","kind":"HOW","packages":["pkg:npm/react-dom@18.3.1"],"schemaVersion":1,"symbols":["react-dom/server.renderToPipeableStream","react-dom/server.renderToString","react-dom/server.renderToNodeStream","react-dom/client.createRoot","react-dom.render"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/react-dom@18.3.1"],"schemaVersion":1,"symbols":["react-dom/server.renderToPipeableStream","react-dom/server.renderToString","react-dom/server.renderToNodeStream","react-dom/client.createRoot","react-dom.render"],"verifierAdapter":"node-typescript@1"}
{
"name": "npm-react-dom-v18",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "npm-react-dom-v18",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
}
}
}
{
"name": "npm-react-dom-v18",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
}
}
import assert from 'node:assert/strict';
import { Writable, Readable } from 'node:stream';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { renderToPipeableStream, renderToString, renderToNodeStream } from 'react-dom/server';
import * as ReactDOMClient from 'react-dom/client';
import * as ReactDOM from 'react-dom';
// 1. renderToPipeableStream returns a synchronous controller object { pipe, abort }, NOT a stream or Promise
const element = React.createElement('div', { id: 'app' }, 'React 18 SSR');
let shellReadyCalled = false;
let allReadyCalled = false;
const controller = renderToPipeableStream(element, {
onShellReady() {
shellReadyCalled = true;
},
onAllReady() {
allReadyCalled = true;
}
});
// A naive model expects a Readable stream instance or a Promise resolving to a stream
assert.equal(typeof controller.pipe, 'function', 'pipe should be a function');
assert.equal(typeof controller.abort, 'function', 'abort should be a function');
assert.equal(controller instanceof Readable, false, 'controller is not an instance of stream.Readable');
assert.equal(controller instanceof Promise, false, 'controller is not a Promise');
// 2. controller.pipe(destination) takes a Writable destination and returns that same destination
const chunks = [];
const destination = new Writable({
write(chunk, encoding, callback) {
chunks.push(chunk);
callback();
}
});
const pipeReturnValue = controller.pipe(destination);
assert.equal(pipeReturnValue, destination, 'controller.pipe returns the passed destination stream');
await new Promise((resolve) => destination.on('finish', resolve));
const outputHtml = Buffer.concat(chunks).toString('utf8');
assert.equal(outputHtml, '<div id="app">React 18 SSR</div>');
assert.equal(shellReadyCalled, true, 'onShellReady callback fired');
assert.equal(allReadyCalled, true, 'onAllReady callback fired');
// 3. renderToString with Suspense: does not throw or await, instead renders fallback HTML with diagnostic template
function SuspendedComponent() {
throw new Promise(() => {});
}
const suspenseTree = React.createElement(
React.Suspense,
{ fallback: React.createElement('p', null, 'Loading...') },
React.createElement(SuspendedComponent)
);
const stringHtml = renderToString(suspenseTree);
assert.match(stringHtml, /<p>Loading\.\.\.<\/p>/, 'Suspense fallback is emitted synchronously');
assert.match(stringHtml, /<!--\$!-->/, 'Emits suspense boundary marker');
assert.match(stringHtml, /data-msg="The server did not finish this Suspense boundary/, 'Emits diagnostic template message');
// 4. API surface differences across major versions
// In React 18 Node react-dom/server: renderToNodeStream exists (deprecated in 18, removed in 19)
assert.equal(typeof renderToNodeStream, 'function', 'renderToNodeStream exists in React 18');
assert.equal(typeof ReactDOMServer.renderToReadableStream, 'undefined', 'renderToReadableStream is not on Node server entrypoint');
// In React 18 react-dom: legacy render/hydrate/unmount exist (removed in React 19)
assert.equal(typeof ReactDOM.render, 'function', 'legacy ReactDOM.render is exported in React 18');
assert.equal(typeof ReactDOM.hydrate, 'function', 'legacy ReactDOM.hydrate is exported in React 18');
assert.equal(typeof ReactDOM.unmountComponentAtNode, 'function', 'legacy ReactDOM.unmountComponentAtNode is exported in React 18');
// In React 18 react-dom/client: modern root entrypoints are exported
assert.equal(typeof ReactDOMClient.createRoot, 'function', 'createRoot is exported from react-dom/client');
assert.equal(typeof ReactDOMClient.hydrateRoot, 'function', 'hydrateRoot is exported from react-dom/client');