You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
But with that approach, React is going to flush all of the intermediate loading states. It would be nice if we could just wait for the render to be completed so we just cache the final response. We could do something like this:
But this isn't perfect either, because now the code that is rendering the component has to know details about the underlying stream, and we have to restructure the rendering code if the stream changes. It would be nice if the first example could just always work.
There's a secondary problem in that not all of the supported stream implementations support the same signals. ReadableStream for example, doesn't have a direct alternative for cork and flush. These signals are still very useful to implement in user-space though, especially in environments like Cloudflare Workers where not buffering React's writes will significantly increase the number of transmitted bytes.
We tried adding framework specific support instead, but we haven't received much feedback (which is understandable, since the React team has many other priorities), and our needs seem generic enough anyway that perhaps it makes sense for React to expose this at a slightly lower level anyway.
Proposal
I'd like to propose that React add support for a very simple ReactStreamWriter interface. Rather than being driven by external signals like onCompleteAll, it would be completely driven by backpressure from the streams it is writing to. Here, backpressure could be achieved via a simple isBuffering flag that indicates whether React can assume that the next write will be processed by the client imminently.
The interface itself could be very simple. Here's a place to start bikeshedding:
interfaceReactStreamWriter{// Output a chunk. Assume UTF-8 encoding.write(chunk: Uint8Array): void// Signal that React is done writing, with an optional errorclose(error: Error|undefined): void// Toggle whether writes should be soft buffered (because React is burst-writing)cork(toggle: boolean): void// Signal that React is done writing for a while, and that now would be a// good time to flush any underlying buffers (e.g. for compression)flush(): void// A callback to run when `isBuffering` changes. Returns a function// that can be called to unsubscribe.subscribe(callback: ()=>void): ()=>void// Whether the next write will be processed by a client imminentlyisBuffering: boolean}
Note: While this looks fairly similar to the Node.js Writable interface, there is no guarantee on what Writable methods or behaviors React will rely on in the future, and so non-Node.js platforms would need to implement a fairly complete Writable polyfill, which is a fairly complicated endeavor. This simple interface dramatically reduces the scope.
With this interface, React would use the current value of isBuffering to know whether it should write intermediate data. If the writer is buffering, writes won't be seen imminently, so React should wait to try to write more up-to-date data later, or wait for the rendering work to fully complete.
The primary purpose would be for advanced cases (e.g. meta frameworks) to more easily ensure that the optimal stream can be generated in the face of many variables, like deployment targets, caching, runtime variables, and so on. These advanced users would be responsible for ensuring that the signals properly flow in both directions (where applicable) so that the most ideal stream is generated for each situation.
Revisiting the Example
Now, we could create a simple ReactStreamWriters like so:
// A `ReactStreamWriter` that always buffersclassBufferedWriterimplementsReactStreamWriter{_writer: ReactStreamWriterconstructor(writer: ReactStreamWriter){this._writer=writer}write(chunk: Uint8Array){this._writer.write(chunk)}close(error?: Error){this._writer.close(error)}cork(toggle: boolean){this._writer.cork(toggle)}flush(){// noop}subscribe(callback: ()=>void){// noop, `isBuffering` will never changereturn()=>{}}getisBuffering(){// Writes to `BufferedWriter` will _never_ be processed imminentlyreturntrue}}// A `ReactStreamWriter` that writes to a Node.js `Writable`classWritableStreamWriterimplementsReactStreamWriter{_writer: Writable_subscriptions: Set<()=>void>constructor(writable: Writable){this._writable=writablethis._subscriptions=newSet()this._writable.addListener('drain',()=>{this._subscriptions.values().forEach(callback=>callback())})}write(chunk: Uint8Array){this._writable.write(chunk)}close(error?: Error){if(error){this._writable.destroy(error)}else{this._writable.end()}this._subscriptions.clear()}cork(toggle: boolean){if(toggle){this._writable.cork()}else{this._writable.uncork()}}flush(){if(typeofthis._writable.flush==='function'){this._writable.flush()}}subscribe(callback: ()=>void){this._subscriptions.add(callback)return()=>{this._subscriptions.delete(callback)}}getisBuffering(){// Writes to `WritableStreamWriter` may be processed imminently,// depending on the underlying stream backpressurereturnthis._writable.writableLength>=this._writable.writableHighWaterMark}}
Similar classes could be written for e.g. WHATWG ReadableStream. These could be used like so:
// Create a writer, somehow. Maybe it's a Node.js Writable, maybe it's a `ReadableStream` 🤷♂️ constwriter=newBufferedWriter(newWritableStreamWriter(fs.createWriteStream('./foo.html'))ReactDOMServer.renderToStream(<App/>).pipe(writer)
In the future, when React supports piping to multiple streams, a different writer implementation with completely different characteristics could be .pipe()'d too.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
The Problem
Currently, React supports streaming rendering to Node.js
Writable
s and WHATWGReadableStream
s.These are great, but not perfect. For example, what if you want to pre-render a response to a file? You could do this:
But with that approach, React is going to flush all of the intermediate loading states. It would be nice if we could just wait for the render to be completed so we just cache the final response. We could do something like this:
But this isn't perfect either, because now the code that is rendering the component has to know details about the underlying stream, and we have to restructure the rendering code if the stream changes. It would be nice if the first example could just always work.
There's a secondary problem in that not all of the supported stream implementations support the same signals.
ReadableStream
for example, doesn't have a direct alternative forcork
andflush
. These signals are still very useful to implement in user-space though, especially in environments like Cloudflare Workers where not buffering React's writes will significantly increase the number of transmitted bytes.We tried adding framework specific support instead, but we haven't received much feedback (which is understandable, since the React team has many other priorities), and our needs seem generic enough anyway that perhaps it makes sense for React to expose this at a slightly lower level anyway.
Proposal
I'd like to propose that React add support for a very simple
ReactStreamWriter
interface. Rather than being driven by external signals likeonCompleteAll
, it would be completely driven by backpressure from the streams it is writing to. Here, backpressure could be achieved via a simpleisBuffering
flag that indicates whether React can assume that the next write will be processed by the client imminently.The interface itself could be very simple. Here's a place to start bikeshedding:
With this interface, React would use the current value of
isBuffering
to know whether it should write intermediate data. If the writer is buffering, writes won't be seen imminently, so React should wait to try to write more up-to-date data later, or wait for the rendering work to fully complete.The primary purpose would be for advanced cases (e.g. meta frameworks) to more easily ensure that the optimal stream can be generated in the face of many variables, like deployment targets, caching, runtime variables, and so on. These advanced users would be responsible for ensuring that the signals properly flow in both directions (where applicable) so that the most ideal stream is generated for each situation.
Revisiting the Example
Now, we could create a simple
ReactStreamWriter
s like so:Similar classes could be written for e.g. WHATWG
ReadableStream
. These could be used like so:In the future, when React supports piping to multiple streams, a different
writer
implementation with completely different characteristics could be.pipe()
'd too.Beta Was this translation helpful? Give feedback.
All reactions