Skip to content

Commit e83e1dc

Browse files
add support for streaming react components returned from async render function
1 parent 46f92a6 commit e83e1dc

File tree

3 files changed

+62
-35
lines changed

3 files changed

+62
-35
lines changed

node_package/src/ReactOnRailsRSC.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ const stringToStream = (str: string) => {
2121
return stream;
2222
};
2323

24-
const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRenderParams): Readable => {
24+
const streamRenderRSCComponent = (
25+
reactRenderingResult: ReactElement | Promise<ReactElement | string>,
26+
options: RSCRenderParams,
27+
): Readable => {
2528
const { throwJsErrors, reactClientManifestFileName } = options;
2629
const renderState: StreamRenderState = {
2730
result: null,
@@ -31,8 +34,8 @@ const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRender
3134

3235
const { pipeToTransform, readableStream, emitError } =
3336
transformRenderStreamChunksToResultObject(renderState);
34-
loadReactClientManifest(reactClientManifestFileName)
35-
.then((reactClientManifest) => {
37+
Promise.all([loadReactClientManifest(reactClientManifestFileName), reactRenderingResult])
38+
.then(([reactClientManifest, reactElement]) => {
3639
const rscStream = renderToPipeableStream(reactElement, reactClientManifest, {
3740
onError: (err) => {
3841
const error = convertToError(err);

node_package/src/streamServerRenderedReactComponent.ts

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ReactElement } from 'react';
44

55
import ComponentRegistry from './ComponentRegistry';
66
import createReactOutput from './createReactOutput';
7-
import { isPromise, isServerRenderHash } from './isServerRenderResult';
7+
import { isServerRenderHash } from './isServerRenderResult';
88
import buildConsoleReplay from './buildConsoleReplay';
99
import handleError from './handleError';
1010
import { createResultObject, convertToError, validateComponent } from './serverRenderUtils';
@@ -128,7 +128,10 @@ export const transformRenderStreamChunksToResultObject = (renderState: StreamRen
128128
return { readableStream, pipeToTransform, writeChunk, emitError, endStream };
129129
};
130130

131-
const streamRenderReactComponent = (reactRenderingResult: ReactElement, options: RenderParams) => {
131+
const streamRenderReactComponent = (
132+
reactRenderingResult: ReactElement | Promise<ReactElement | string>,
133+
options: RenderParams,
134+
) => {
132135
const { name: componentName, throwJsErrors, domNodeId } = options;
133136
const renderState: StreamRenderState = {
134137
result: null,
@@ -139,42 +142,63 @@ const streamRenderReactComponent = (reactRenderingResult: ReactElement, options:
139142
const { readableStream, pipeToTransform, writeChunk, emitError, endStream } =
140143
transformRenderStreamChunksToResultObject(renderState);
141144

142-
const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderingResult, {
143-
onShellError(e) {
144-
const error = convertToError(e);
145-
renderState.hasErrors = true;
146-
renderState.error = error;
145+
const onShellError = (e: unknown) => {
146+
const error = convertToError(e);
147+
renderState.hasErrors = true;
148+
renderState.error = error;
147149

148-
if (throwJsErrors) {
149-
emitError(error);
150-
}
150+
if (throwJsErrors) {
151+
emitError(error);
152+
}
151153

152-
const errorHtml = handleError({ e: error, name: componentName, serverSide: true });
153-
writeChunk(errorHtml);
154-
endStream();
155-
},
156-
onShellReady() {
157-
renderState.isShellReady = true;
158-
pipeToTransform(renderingStream);
159-
},
160-
onError(e) {
161-
if (!renderState.isShellReady) {
154+
const errorHtml = handleError({ e: error, name: componentName, serverSide: true });
155+
writeChunk(errorHtml);
156+
endStream();
157+
};
158+
159+
Promise.resolve(reactRenderingResult)
160+
.then((reactRenderedElement) => {
161+
if (typeof reactRenderedElement === 'string') {
162+
console.error(
163+
`Error: stream_react_component helper received a string instead of a React component for component "${componentName}".\n` +
164+
'To benefit from React on Rails Pro streaming feature, your render function should return a React component.\n' +
165+
'Do not call ReactDOMServer.renderToString() inside the render function as this defeats the purpose of streaming.\n',
166+
);
167+
168+
writeChunk(reactRenderedElement);
169+
endStream();
162170
return;
163171
}
164-
const error = convertToError(e);
165-
if (throwJsErrors) {
166-
emitError(error);
167-
}
168-
renderState.hasErrors = true;
169-
renderState.error = error;
170-
},
171-
identifierPrefix: domNodeId,
172-
});
172+
173+
const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderedElement, {
174+
onShellError,
175+
onShellReady() {
176+
renderState.isShellReady = true;
177+
pipeToTransform(renderingStream);
178+
},
179+
onError(e) {
180+
if (!renderState.isShellReady) {
181+
return;
182+
}
183+
const error = convertToError(e);
184+
if (throwJsErrors) {
185+
emitError(error);
186+
}
187+
renderState.hasErrors = true;
188+
renderState.error = error;
189+
},
190+
identifierPrefix: domNodeId,
191+
});
192+
})
193+
.catch(onShellError);
173194

174195
return readableStream;
175196
};
176197

177-
type StreamRenderer<T, P extends RenderParams> = (reactElement: ReactElement, options: P) => T;
198+
type StreamRenderer<T, P extends RenderParams> = (
199+
reactElement: ReactElement | Promise<ReactElement | string>,
200+
options: P,
201+
) => T;
178202

179203
export const streamServerRenderedComponent = <T, P extends RenderParams>(
180204
options: P,
@@ -194,7 +218,7 @@ export const streamServerRenderedComponent = <T, P extends RenderParams>(
194218
railsContext,
195219
});
196220

197-
if (isServerRenderHash(reactRenderingResult) || isPromise(reactRenderingResult)) {
221+
if (isServerRenderHash(reactRenderingResult)) {
198222
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
199223
}
200224

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
"target": "es5",
1515
"typeRoots": ["./node_modules/@types", "./node_package/types"]
1616
},
17-
"include": ["node_package/src/**/*", "node_package/types/**/*", "node_package/tests/**/*"]
17+
"include": ["node_package/src/**/*", "node_package/types/**/*"]
1818
}

0 commit comments

Comments
 (0)