Skip to content

Commit acaef11

Browse files
add support for returning promise of react component from render function
1 parent f7651d0 commit acaef11

File tree

6 files changed

+102
-69
lines changed

6 files changed

+102
-69
lines changed

node_package/src/ReactOnRails.client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ ctx.ReactOnRails = {
341341
resetOptions(): void {
342342
this.options = Object.assign({}, DEFAULT_OPTIONS);
343343
},
344+
345+
isRSCBundle: false,
344346
};
345347

346348
ctx.ReactOnRails.resetOptions();

node_package/src/ReactOnRailsRSC.ts

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

24-
const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRenderParams): Readable => {
24+
const streamRenderRSCComponent = (reactRenderingResult: ReactElement | Promise<ReactElement | string>, options: RSCRenderParams): Readable => {
2525
const { throwJsErrors, reactClientManifestFileName } = options;
2626
const renderState: StreamRenderState = {
2727
result: null,
2828
hasErrors: false,
2929
isShellReady: true,
3030
};
3131

32-
const { pipeToTransform, readableStream, emitError } =
33-
transformRenderStreamChunksToResultObject(renderState);
34-
loadJsonFile(reactClientManifestFileName)
35-
.then((reactClientManifest) => {
32+
const { pipeToTransform, readableStream, emitError } = transformRenderStreamChunksToResultObject(renderState);
33+
Promise.all([loadJsonFile(reactClientManifestFileName), reactRenderingResult])
34+
.then(([reactClientManifest, reactElement]) => {
3635
const rscStream = renderToPipeableStream(reactElement, reactClientManifest, {
3736
onError: (err) => {
3837
const error = convertToError(err);
@@ -65,5 +64,7 @@ ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
6564
}
6665
};
6766

67+
ReactOnRails.isRSCBundle = true;
68+
6869
export * from './types';
6970
export default ReactOnRails;

node_package/src/createReactOutput.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ import type {
88
} from './types/index';
99
import { isServerRenderHash, isPromise } from './isServerRenderResult';
1010

11+
export function createReactElementFromRenderFunction(
12+
renderFunctionResult: ReactComponent,
13+
name: string,
14+
props: Record<string, unknown> | undefined,
15+
): React.ReactElement {
16+
if (React.isValidElement(renderFunctionResult)) {
17+
// If already a ReactElement, then just return it.
18+
console.error(
19+
`Warning: ReactOnRails: Your registered render-function (ReactOnRails.register) for ${name}
20+
incorrectly returned a React Element (JSX). Instead, return a React Function Component by
21+
wrapping your JSX in a function. ReactOnRails v13 will throw error on this, as React Hooks do not
22+
work if you return JSX. Update by wrapping the result JSX of ${name} in a fat arrow function.`);
23+
return renderFunctionResult;
24+
}
25+
26+
// If a component, then wrap in an element
27+
const reactComponent = renderFunctionResult as ReactComponent;
28+
return React.createElement(reactComponent, props);
29+
}
30+
1131
/**
1232
* Logic to either call the renderFunction or call React.createElement to get the
1333
* React.Component
@@ -62,23 +82,13 @@ export default function createReactOutput({
6282
if (isPromise(renderFunctionResult as CreateReactOutputResult)) {
6383
// We just return at this point, because calling function knows how to handle this case and
6484
// we can't call React.createElement with this type of Object.
65-
return renderFunctionResult as Promise<string>;
66-
}
67-
68-
if (React.isValidElement(renderFunctionResult)) {
69-
// If already a ReactElement, then just return it.
70-
console.error(
71-
`Warning: ReactOnRails: Your registered render-function (ReactOnRails.register) for ${name}
72-
incorrectly returned a React Element (JSX). Instead, return a React Function Component by
73-
wrapping your JSX in a function. ReactOnRails v13 will throw error on this, as React Hooks do not
74-
work if you return JSX. Update by wrapping the result JSX of ${name} in a fat arrow function.`,
75-
);
76-
return renderFunctionResult;
85+
return (renderFunctionResult as Promise<string | ReactComponent>).then((result) => {
86+
if (typeof result === 'string') return result;
87+
return createReactElementFromRenderFunction(result, name, props)
88+
});
7789
}
7890

79-
// If a component, then wrap in an element
80-
const reactComponent = renderFunctionResult as ReactComponent;
81-
return React.createElement(reactComponent, props);
91+
return createReactElementFromRenderFunction(renderFunctionResult as ReactComponent, name, props);
8292
}
8393
// else
8494
return React.createElement(component as ReactComponent, props);

node_package/src/serverRenderReactComponent.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,6 @@ function processServerRenderHash(result: ServerRenderResult, options: RenderOpti
4242
return { result: htmlResult, hasErrors };
4343
}
4444

45-
function processPromise(
46-
result: Promise<string>,
47-
renderingReturnsPromises: boolean,
48-
): Promise<string> | string {
49-
if (!renderingReturnsPromises) {
50-
console.error(
51-
'Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.',
52-
);
53-
// If the app is using server rendering with ExecJS, then the promise will not be awaited.
54-
// And when a promise is passed to JSON.stringify, it will be converted to '{}'.
55-
return '{}';
56-
}
57-
return result;
58-
}
59-
6045
function processReactElement(result: ReactElement): string {
6146
try {
6247
return ReactDOMServer.renderToString(result);
@@ -68,6 +53,24 @@ as a renderFunction and not a simple React Function Component.`);
6853
}
6954
}
7055

56+
function processPromise(
57+
result: Promise<string | ReactElement>,
58+
renderingReturnsPromises: boolean,
59+
): Promise<string> | string {
60+
if (!renderingReturnsPromises) {
61+
console.error('Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.');
62+
// If the app is using server rendering with ExecJS, then the promise will not be awaited.
63+
// And when a promise is passed to JSON.stringify, it will be converted to '{}'.
64+
return '{}';
65+
}
66+
return result.then((result) => {
67+
if (typeof result !== 'string') {
68+
return processReactElement(result);
69+
}
70+
return result;
71+
});
72+
}
73+
7174
function processRenderingResult(result: CreateReactOutputResult, options: RenderOptions): RenderState {
7275
if (isServerRenderHash(result)) {
7376
return processServerRenderHash(result, options);

node_package/src/streamServerRenderedReactComponent.ts

Lines changed: 49 additions & 32 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';
@@ -158,7 +158,7 @@ export const transformRenderStreamChunksToResultObject = (renderState: StreamRen
158158
return { readableStream, pipeToTransform, writeChunk, emitError, endStream };
159159
};
160160

161-
const streamRenderReactComponent = (reactRenderingResult: ReactElement, options: RenderParams) => {
161+
const streamRenderReactComponent = (reactRenderingResult: ReactElement | Promise<ReactElement | string>, options: RenderParams) => {
162162
const { name: componentName, throwJsErrors, domNodeId } = options;
163163
const renderState: StreamRenderState = {
164164
result: null,
@@ -169,42 +169,59 @@ const streamRenderReactComponent = (reactRenderingResult: ReactElement, options:
169169
const { readableStream, pipeToTransform, writeChunk, emitError, endStream } =
170170
transformRenderStreamChunksToResultObject(renderState);
171171

172-
const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderingResult, {
173-
onShellError(e) {
174-
const error = convertToError(e);
175-
renderState.hasErrors = true;
176-
renderState.error = error;
172+
Promise.resolve(reactRenderingResult).then(reactRenderedElement => {
173+
if (typeof reactRenderedElement === 'string') {
174+
console.error(
175+
`Error: stream_react_component helper received a string instead of a React component for component "${componentName}".\n` +
176+
'To benefit from React on Rails Pro streaming feature, your render function should return a React component.\n' +
177+
'Do not call ReactDOMServer.renderToString() inside the render function as this defeats the purpose of streaming.\n'
178+
);
177179

178-
if (throwJsErrors) {
179-
emitError(error);
180-
}
181-
182-
const errorHtml = handleError({ e: error, name: componentName, serverSide: true });
183-
writeChunk(errorHtml);
180+
writeChunk(reactRenderedElement);
184181
endStream();
185-
},
186-
onShellReady() {
187-
renderState.isShellReady = true;
188-
pipeToTransform(renderingStream);
189-
},
190-
onError(e) {
191-
if (!renderState.isShellReady) {
192-
return;
193-
}
194-
const error = convertToError(e);
195-
if (throwJsErrors) {
196-
emitError(error);
197-
}
198-
renderState.hasErrors = true;
199-
renderState.error = error;
200-
},
201-
identifierPrefix: domNodeId,
182+
return;
183+
}
184+
185+
const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderedElement, {
186+
onShellError(e) {
187+
const error = convertToError(e);
188+
renderState.hasErrors = true;
189+
renderState.error = error;
190+
191+
if (throwJsErrors) {
192+
emitError(error);
193+
}
194+
195+
const errorHtml = handleError({ e: error, name: componentName, serverSide: true });
196+
writeChunk(errorHtml);
197+
endStream();
198+
},
199+
onShellReady() {
200+
renderState.isShellReady = true;
201+
pipeToTransform(renderingStream);
202+
},
203+
onError(e) {
204+
if (!renderState.isShellReady) {
205+
return;
206+
}
207+
const error = convertToError(e);
208+
if (throwJsErrors) {
209+
emitError(error);
210+
}
211+
renderState.hasErrors = true;
212+
renderState.error = error;
213+
},
214+
identifierPrefix: domNodeId,
215+
});
202216
});
203217

204218
return readableStream;
205219
};
206220

207-
type StreamRenderer<T, P extends RenderParams> = (reactElement: ReactElement, options: P) => T;
221+
type StreamRenderer<T, P extends RenderParams> = (
222+
reactElement: ReactElement | Promise<ReactElement | string>,
223+
options: P,
224+
) => T;
208225

209226
export const streamServerRenderedComponent = <T, P extends RenderParams>(
210227
options: P,
@@ -224,7 +241,7 @@ export const streamServerRenderedComponent = <T, P extends RenderParams>(
224241
railsContext,
225242
});
226243

227-
if (isServerRenderHash(reactRenderingResult) || isPromise(reactRenderingResult)) {
244+
if (isServerRenderHash(reactRenderingResult)) {
228245
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
229246
}
230247

node_package/src/types/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ interface ServerRenderResult {
4949
error?: Error;
5050
}
5151

52-
type CreateReactOutputResult = ServerRenderResult | ReactElement | Promise<string>;
52+
type CreateReactOutputResult = ServerRenderResult | ReactElement | Promise<string | ReactElement>;
5353

54-
type RenderFunctionResult = ReactComponent | ServerRenderResult | Promise<string>;
54+
type RenderFunctionResult = ReactComponent | ServerRenderResult | Promise<string | ReactComponent>;
5555

5656
/**
5757
* Render functions are used to create dynamic React components or server-rendered HTML with side effects.

0 commit comments

Comments
 (0)