Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/docs/reference-react-dom-node-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ The `ReactDOMNodeStream` object allows you to render your components in Node.js
### `renderToStream()`

```javascript
ReactDOMNodeStream.renderToStream(element)
ReactDOMNodeStream.renderToStream(element, [{ highWaterMark }])
```

Render a React element to its initial HTML. This should only be used in Node.js; it will not work in the browser, since the browser does not support Node.js streams. React will return a [Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) that outputs an HTML string. The HTML output by this stream will be exactly equal to what [`ReactDOMServer.renderToString`](https://facebook.github.io/react/docs/react-dom-server.html#rendertostring) would return. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.

`renderToStream` takes an optional second argument with stream options. `highWaterMark` is currently the only available option, and it specifies how many bytes of memory to use to buffer the resulting markup before pausing the render. For more information on how this argument works, please see the [section on Buffering](https://nodejs.org/api/stream.html#stream_buffering) in the Node stream documentation.

If you call [`ReactDOM.render()`](/react/docs/react-dom.html#render) on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

Note that the stream returned from this method will return a byte stream encoded in utf-8. If you need a stream in another encoding, take a look a project like [iconv-lite](https://www.npmjs.com/package/iconv-lite), which provides transform streams for transcoding text.
Expand All @@ -38,9 +40,11 @@ Note that the stream returned from this method will return a byte stream encoded
### `renderToStaticStream()`

```javascript
ReactDOMNodeStream.renderToStaticStream(element)
ReactDOMNodeStream.renderToStaticStream(element, [{ highWaterMark }])
```

Similar to [`renderToStream`](#rendertostream), except this doesn't create extra DOM attributes such as `data-reactid`, that React uses internally. This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save lots of bytes.

`renderToStaticStream` takes an optional second argument with stream options. `highWaterMark` is currently the only available option, and it specifies how many bytes of memory to use to buffer the resulting markup before pausing the render. For more information on how this argument works, please see the [section on Buffering](https://nodejs.org/api/stream.html#stream_buffering) in the Node stream documentation.

Note that the stream returned from this method will return a byte stream encoded in utf-8. If you need a stream in another encoding, take a look a project like [iconv-lite](https://www.npmjs.com/package/iconv-lite), which provides transform streams for transcoding text.
3 changes: 3 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,9 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* should reconnect Pure Component to Bare Element
* should reconnect Bare Element to Bare Element
* should reconnect a div with a number and string version of number
* should obey low highWaterMark when streaming
* should by default stream 15K in one chunk
* should by default stream 17K in two chunks

src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
* updates a mounted text component in place
Expand Down
14 changes: 6 additions & 8 deletions src/renderers/dom/ReactDOMNodeStreamRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ var Readable = require('stream').Readable;

// This is a Readable Node.js stream which wraps the ReactDOMPartialRenderer.
class ReactMarkupReadableStream extends Readable {
constructor(element, makeStaticMarkup) {
// Calls the stream.Readable(options) constructor. Consider exposing built-in
// features like highWaterMark in the future.
super({});
constructor(element, makeStaticMarkup, {highWaterMark} = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason we're destructing in these constructors? It doesn't look like highWaterMark is used here, so maybe we can just do:

constructor(element, makeStaticMarkup, options = {}) {
   super(options);
   // ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question!

The reason I'm destructing highWaterMark is that there are several other options that can be passed to the Readable constructor (documented here), and those options will make the stream behave incorrectly. For example, if you pass an encoding argument to the Readable constructor, the stream will output gibberish. Passing a read argument will make the stream not render anything.

If we pass the options object through unfiltered, the client of this class could accidentally make it malfunction, whereas if we only pass highWaterMark, we will be OK. Does that make sense?

super({highWaterMark});
this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
}

Expand All @@ -39,25 +37,25 @@ class ReactMarkupReadableStream extends Readable {
* server.
* See https://facebook.github.io/react/docs/react-dom-stream.html#rendertostream
*/
function renderToStream(element) {
function renderToStream(element, {highWaterMark} = {}) {
invariant(
React.isValidElement(element),
'renderToStream(): You must pass a valid ReactElement.',
);
return new ReactMarkupReadableStream(element, false);
return new ReactMarkupReadableStream(element, false, {highWaterMark});
}

/**
* Similar to renderToStream, except this doesn't create extra DOM attributes
* such as data-react-id that React uses internally.
* See https://facebook.github.io/react/docs/react-dom-stream.html#rendertostaticstream
*/
function renderToStaticStream(element) {
function renderToStaticStream(element, {highWaterMark} = {}) {
invariant(
React.isValidElement(element),
'renderToStaticStream(): You must pass a valid ReactElement.',
);
return new ReactMarkupReadableStream(element, true);
return new ReactMarkupReadableStream(element, true, {highWaterMark});
}

module.exports = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2172,4 +2172,82 @@ describe('ReactDOMServerIntegration', () => {
<div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
));
});

describe('stream options', function() {
// streaming only exists in React 16.
if (!ReactDOMFeatureFlags.useFiber) {
return;
}

class ChunkCountWritable extends stream.Writable {
constructor(options) {
super(options);
this.chunkCount = 0;
}

_write(chunk, encoding, cb) {
this.chunkCount += 1;
cb();
}
}

it('should obey low highWaterMark when streaming', () => {
const element = (
<div>
Some text to render<span>and more text</span>
<span>and even more</span>
</div>
);

const output = new ChunkCountWritable();
return new Promise(resolve => {
// if we set the highWaterMark very low, then we should get more than one
// chunk written to the writable stream.
ReactDOMNodeStream.renderToStream(element, {highWaterMark: 5})
.pipe(output)
.on('finish', resolve);
}).then(() => {
expect(output.chunkCount).toBeGreaterThan(1);
});
});

// gets an element of **approximately** kilobytes number of characters.
// because of elements and react-ids and such, the result will be a little
// bigger than the requested size.
function getElementOfSize(kilobytes) {
const kilobyteOfText = 'a'.repeat(1024 - '<div></div>'.length);
const children = [];
for (let i = 0; i < kilobytes; i++) {
children.push(<div key={i}>{kilobyteOfText}</div>);
}
return <div>{children}</div>;
}

it('should by default stream 15K in one chunk', () => {
// an element of about 15K should fit in 16K and be one chunk with
// the standard highWaterMark of 16K.

const output = new ChunkCountWritable();
return new Promise(resolve => {
ReactDOMNodeStream.renderToStream(getElementOfSize(15))
.pipe(output)
.on('finish', resolve);
}).then(() => {
expect(output.chunkCount).toBe(1);
});
});

it('should by default stream 17K in two chunks', () => {
// an element of about 17K should not fit in 16K and be two chunks with
// the standard highWaterMark of 16K.
const output = new ChunkCountWritable();
return new Promise(resolve => {
ReactDOMNodeStream.renderToStream(getElementOfSize(17))
.pipe(output)
.on('finish', resolve);
}).then(() => {
expect(output.chunkCount).toBe(2);
});
});
});
});