Skip to content

Commit 5c99bc4

Browse files
Refactor ReactOnRails to support React Server Components (RSC) registration and rendering
1 parent cc1cdbf commit 5c99bc4

11 files changed

+293
-140
lines changed

node_package/src/RSCServerRoot.ts

+90-58
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,90 @@
1-
// import * as React from 'react';
2-
// import { createFromNodeStream } from 'react-on-rails-rsc/client.node';
3-
// import transformRSCStream from './transformRSCNodeStreamAndReplayConsoleLogs';
4-
// import loadJsonFile from './loadJsonFile';
5-
6-
// if (!('use' in React && typeof React.use === 'function')) {
7-
// throw new Error('React.use is not defined. Please ensure you are using React 18 with experimental features enabled or React 19+ to use server components.');
8-
// }
9-
10-
// const { use } = React;
11-
12-
// export type RSCServerRootProps = {
13-
// getRscPromise: NodeJS.ReadableStream,
14-
// reactClientManifestFileName: string,
15-
// reactServerManifestFileName: string,
16-
// }
17-
18-
// const createFromFetch = (stream: NodeJS.ReadableStream, ssrManifest: Record<string, unknown>) => {
19-
// const transformedStream = transformRSCStream(stream);
20-
// return createFromNodeStream(transformedStream, ssrManifest);
21-
// }
22-
23-
// const createSSRManifest = (reactServerManifestFileName: string, reactClientManifestFileName: string) => {
24-
// const reactServerManifest = loadJsonFile(reactServerManifestFileName);
25-
// const reactClientManifest = loadJsonFile(reactClientManifestFileName);
26-
27-
// const ssrManifest = {
28-
// moduleLoading: {
29-
// prefix: "/webpack/development/",
30-
// crossOrigin: null,
31-
// },
32-
// moduleMap: {} as Record<string, unknown>,
33-
// };
34-
35-
// Object.entries(reactClientManifest).forEach(([aboluteFileUrl, clientFileBundlingInfo]) => {
36-
// const serverFileBundlingInfo = reactServerManifest[aboluteFileUrl];
37-
// ssrManifest.moduleMap[(clientFileBundlingInfo as { id: string }).id] = {
38-
// '*': {
39-
// id: (serverFileBundlingInfo as { id: string }).id,
40-
// chunks: (serverFileBundlingInfo as { chunks: string[] }).chunks,
41-
// name: '*',
42-
// }
43-
// };
44-
// });
45-
46-
// return ssrManifest;
47-
// }
48-
49-
// const RSCServerRoot = ({
50-
// getRscPromise,
51-
// reactClientManifestFileName,
52-
// reactServerManifestFileName,
53-
// }: RSCServerRootProps) => {
54-
// const ssrManifest = createSSRManifest(reactServerManifestFileName, reactClientManifestFileName);
55-
// return use(createFromFetch(getRscPromise, ssrManifest));
56-
// };
57-
58-
// export default RSCServerRoot;
1+
import * as React from 'react';
2+
import { createFromNodeStream } from 'react-on-rails-rsc/client.node';
3+
import type { RenderFunction, RailsContext } from './types';
4+
import transformRSCStream from './transformRSCNodeStreamAndReplayConsoleLogs';
5+
import loadJsonFile from './loadJsonFile';
6+
7+
declare global {
8+
function generateRSCPayload(
9+
componentName: string,
10+
props: Record<string, unknown>,
11+
serverSideRSCPayloadParameters: unknown,
12+
): Promise<NodeJS.ReadableStream>;
13+
}
14+
15+
type RSCServerRootProps = {
16+
componentName: string;
17+
componentProps: Record<string, unknown>;
18+
}
19+
20+
if (!('use' in React && typeof React.use === 'function')) {
21+
throw new Error('React.use is not defined. Please ensure you are using React 18 with experimental features enabled or React 19+ to use server components.');
22+
}
23+
24+
const { use } = React;
25+
26+
const createFromReactOnRailsNodeStream = (stream: NodeJS.ReadableStream, ssrManifest: Record<string, unknown>) => {
27+
const transformedStream = transformRSCStream(stream);
28+
return createFromNodeStream(transformedStream, ssrManifest);
29+
}
30+
31+
const createSSRManifest = async (reactServerManifestFileName: string, reactClientManifestFileName: string) => {
32+
const [reactServerManifest, reactClientManifest] = await Promise.all([
33+
loadJsonFile(reactServerManifestFileName),
34+
loadJsonFile(reactClientManifestFileName),
35+
]);
36+
37+
const ssrManifest = {
38+
moduleLoading: {
39+
prefix: "/webpack/development/",
40+
crossOrigin: null,
41+
},
42+
moduleMap: {} as Record<string, unknown>,
43+
};
44+
45+
Object.entries(reactClientManifest).forEach(([aboluteFileUrl, clientFileBundlingInfo]) => {
46+
const serverFileBundlingInfo = reactServerManifest[aboluteFileUrl];
47+
ssrManifest.moduleMap[(clientFileBundlingInfo as { id: string }).id] = {
48+
'*': {
49+
id: (serverFileBundlingInfo as { id: string }).id,
50+
chunks: (serverFileBundlingInfo as { chunks: string[] }).chunks,
51+
name: '*',
52+
}
53+
};
54+
});
55+
56+
return ssrManifest;
57+
}
58+
59+
const RSCServerRoot: RenderFunction = async ({ componentName, componentProps }: RSCServerRootProps, railsContext?: RailsContext) => {
60+
if (!railsContext?.serverSide || !railsContext?.reactClientManifestFileName || !railsContext?.reactServerClientManifestFileName) {
61+
throw new Error(
62+
`${'serverClientManifestFileName and reactServerClientManifestFileName are required. ' +
63+
'Please ensure that React Server Component webpack configurations are properly set ' +
64+
'as stated in the React Server Component tutorial. The received rails context is: '}${ JSON.stringify(railsContext)}`
65+
);
66+
}
67+
68+
if (typeof generateRSCPayload !== 'function') {
69+
throw new Error(
70+
'generateRSCPayload is not defined. Please ensure that you are using at least version 4.0.0 of ' +
71+
'React on Rails Pro and the node renderer, and that ReactOnRailsPro.configuration.enable_rsc_support ' +
72+
'is set to true.'
73+
);
74+
}
75+
76+
const ssrManifest = await createSSRManifest(
77+
railsContext.reactServerClientManifestFileName,
78+
railsContext.reactClientManifestFileName
79+
);
80+
const rscPayloadStream = await generateRSCPayload(
81+
componentName,
82+
componentProps,
83+
railsContext.serverSideRSCPayloadParameters
84+
);
85+
const serverComponentElement = createFromReactOnRailsNodeStream(rscPayloadStream, ssrManifest);
86+
87+
return () => use(serverComponentElement);
88+
};
89+
90+
export default RSCServerRoot;

node_package/src/ReactOnRailsRSC.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ const stringToStream = (str: string) => {
2525
};
2626

2727
const streamRenderRSCComponent = (reactRenderingResult: ReactElement | Promise<ReactElement | string>, options: RSCRenderParams): Readable => {
28-
const { throwJsErrors, reactClientManifestFileName } = options;
28+
const { throwJsErrors } = options;
29+
if (!options.railsContext?.serverSide || !options.railsContext.reactClientManifestFileName) {
30+
throw new Error('Rails context is not available');
31+
}
32+
33+
const { reactClientManifestFileName } = options.railsContext;
2934
const renderState: StreamRenderState = {
3035
result: null,
3136
hasErrors: false,

node_package/src/handleError.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ Message: ${e.message}
6262
${e.stack}`;
6363

6464
const reactElement = React.createElement('pre', null, msg);
65-
return ReactDOMServer.renderToString(reactElement);
65+
if (typeof ReactDOMServer.renderToString === 'function') {
66+
return ReactDOMServer.renderToString(reactElement);
67+
}
68+
return msg;
6669
}
6770

6871
return "undefined";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import ReactOnRails from '../ReactOnRails.client';
2+
import { ReactComponent, RenderFunction } from '../types';
3+
4+
/**
5+
* Registers React Server Components (RSC) with React on Rails for the RSC bundle.
6+
*
7+
* This function handles the registration of components in the RSC bundle context,
8+
* where components are registered directly into the ComponentRegistry without any
9+
* additional wrapping. This is different from the server bundle registration,
10+
* which wraps components with RSCServerRoot.
11+
*
12+
* @param components - Object mapping component names to their implementations
13+
*
14+
* @example
15+
* ```js
16+
* registerServerComponent({
17+
* ServerComponent1: ServerComponent1Component,
18+
* ServerComponent2: ServerComponent2Component
19+
* });
20+
* ```
21+
*/
22+
const registerServerComponent = (
23+
components: { [id: string]: ReactComponent | RenderFunction },
24+
) => ReactOnRails.register(components);
25+
26+
export default registerServerComponent;
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
import ReactOnRails from '../ReactOnRails.client';
2-
import { ReactComponent } from '../types';
2+
import RSCServerRoot from '../RSCServerRoot';
3+
import { ReactComponent, RenderFunction, RailsContext } from '../types';
34

45
/**
5-
* Registers React Server Components (RSC) with React on Rails for both server and RSC bundles.
6-
* Currently, this function behaves identically to ReactOnRails.register, but is introduced to enable
7-
* future RSC-specific functionality without breaking changes.
8-
*
9-
* Future behavior will differ based on bundle type:
10-
*
11-
* RSC Bundle:
12-
* - Components are registered as any other component by adding the component to the ComponentRegistry
13-
*
14-
* Server Bundle:
15-
* - It works like the function defined at `registerServerComponent/client`
16-
* - The function itself is not added to the ComponentRegistry
17-
* - Instead, a RSCServerRoot component is added to the ComponentRegistry
18-
* - This RSCServerRoot component will use the pre-generated RSC payloads from the RSC bundle to
19-
* build the rendering tree of the server component instead of rendering it again
6+
* Registers React Server Components (RSC) with React on Rails for the server bundle.
7+
*
8+
* This function wraps each component with RSCServerRoot, which handles the server-side
9+
* rendering of React Server Components using pre-generated RSC payloads.
2010
*
21-
* This functionality is added now without real implementation to avoid breaking changes in the future.
11+
* The RSCServerRoot component:
12+
* - Uses pre-generated RSC payloads from the RSC bundle
13+
* - Builds the rendering tree of the server component
14+
* - Handles the integration with React's streaming SSR
2215
*
2316
* @param components - Object mapping component names to their implementations
2417
*
@@ -30,8 +23,15 @@ import { ReactComponent } from '../types';
3023
* });
3124
* ```
3225
*/
33-
const registerServerComponent = (components: { [id: string]: ReactComponent }) => {
34-
ReactOnRails.register(components);
26+
const registerServerComponent = (components: { [id: string]: ReactComponent | RenderFunction }) => {
27+
const componentsWrappedInRSCServerRoot: { [id: string]: RenderFunction } = {};
28+
for (const [componentName] of Object.entries(components)) {
29+
componentsWrappedInRSCServerRoot[componentName] = (
30+
componentProps?: unknown,
31+
railsContext?: RailsContext,
32+
) => RSCServerRoot({ componentName, componentProps }, railsContext);
33+
}
34+
return ReactOnRails.register(componentsWrappedInRSCServerRoot);
3535
};
3636

3737
export default registerServerComponent;

node_package/src/serverRenderReactComponent.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { isPromise, isServerRenderHash } from './isServerRenderResult';
77
import buildConsoleReplay from './buildConsoleReplay';
88
import handleError from './handleError';
99
import { createResultObject, convertToError, validateComponent } from './serverRenderUtils';
10-
import type { CreateReactOutputResult, RenderParams, RenderResult, RenderState, RenderOptions, ServerRenderResult, ReactComponent } from './types';
10+
import type { CreateReactOutputResult, RenderParams, RenderResult, RenderState, RenderOptions, ServerRenderResult } from './types';
1111

1212
function processServerRenderHash(result: ServerRenderResult, options: RenderOptions): RenderState {
1313
const { redirectLocation, routeError } = result;
@@ -51,11 +51,11 @@ function processPromise(result: Promise<string | ReactElement>, renderingReturns
5151
// And when a promise is passed to JSON.stringify, it will be converted to '{}'.
5252
return '{}';
5353
}
54-
return result.then((result) => {
55-
if (typeof result !== 'string') {
56-
return processReactElement(result);
54+
return result.then((renderedResult) => {
55+
if (typeof renderedResult !== 'string') {
56+
return processReactElement(renderedResult);
5757
}
58-
return result;
58+
return renderedResult;
5959
});
6060
}
6161

0 commit comments

Comments
 (0)