Skip to content

Commit 745753d

Browse files
Refactor RSC component handling to improve payload key generation
- Updated `createRSCPayloadKey` to remove `domNodeId` dependency for cache key generation. - Modified `getReactServerComponent` functions to accept context parameters more cleanly. - Enhanced documentation for clarity on function responsibilities and parameters. - Adjusted `injectRSCPayload` to align with new cache key structure. - Streamlined RSCProvider integration by removing unnecessary parameters.
1 parent 521c5b3 commit 745753d

File tree

8 files changed

+82
-95
lines changed

8 files changed

+82
-95
lines changed

node_package/src/RSCProvider.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import { RailsContext } from './types/index.ts';
3-
import getReactServerComponent from './getReactServerComponent.client.ts';
2+
import type { ClientGetReactServerComponentProps } from './getReactServerComponent.client.ts';
3+
import { createRSCPayloadKey } from './utils.ts';
44

55
type RSCContextType = {
66
getComponent: (componentName: string, componentProps: unknown) => Promise<React.ReactNode>;
@@ -30,13 +30,9 @@ const RSCContext = React.createContext<RSCContextType | undefined>(undefined);
3030
* for client-side rendering or 'react-on-rails/wrapServerComponentRenderer/server' for server-side rendering.
3131
*/
3232
export const createRSCProvider = ({
33-
railsContext,
3433
getServerComponent,
35-
createRSCPayloadKey,
3634
}: {
37-
railsContext: RailsContext;
38-
getServerComponent: typeof getReactServerComponent;
39-
createRSCPayloadKey: (componentName: string, componentProps: unknown) => string;
35+
getServerComponent: (props: ClientGetReactServerComponentProps) => Promise<React.ReactNode>;
4036
}) => {
4137
const fetchRSCPromises: Record<string, Promise<React.ReactNode>> = {};
4238

@@ -46,7 +42,7 @@ export const createRSCProvider = ({
4642
return fetchRSCPromises[key];
4743
}
4844

49-
const promise = getServerComponent({ componentName, componentProps, railsContext, createRSCPayloadKey });
45+
const promise = getServerComponent({ componentName, componentProps });
5046
fetchRSCPromises[key] = promise;
5147
return promise;
5248
};
@@ -56,9 +52,7 @@ export const createRSCProvider = ({
5652
const promise = getServerComponent({
5753
componentName,
5854
componentProps,
59-
railsContext,
6055
enforceRefetch: true,
61-
createRSCPayloadKey,
6256
});
6357
fetchRSCPromises[key] = promise;
6458
return promise;

node_package/src/RSCRequestTracker.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import {
55
RailsContextWithServerComponentMetadata,
66
} from './types/index.ts';
77

8+
/**
9+
* Global function provided by React on Rails Pro for generating RSC payloads.
10+
*
11+
* This function is injected into the global scope during server-side rendering
12+
* by the RORP rendering request. It handles the actual generation of React Server
13+
* Component payloads on the server side.
14+
*
15+
* @see https://github.com/shakacode/react_on_rails_pro/blob/master/lib/react_on_rails_pro/server_rendering_js_code.rb
16+
*/
817
declare global {
918
function generateRSCPayload(
1019
componentName: string,

node_package/src/getReactServerComponent.client.ts

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { createFromReadableStream } from 'react-on-rails-rsc/client.browser';
3-
import { fetch, wrapInNewPromise } from './utils.ts';
3+
import { createRSCPayloadKey, fetch, wrapInNewPromise } from './utils.ts';
44
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs.ts';
55
import { RailsContext } from './types/index.ts';
66

@@ -10,12 +10,10 @@ declare global {
1010
}
1111
}
1212

13-
type ClientGetReactServerComponentProps = {
13+
export type ClientGetReactServerComponentProps = {
1414
componentName: string;
1515
componentProps: unknown;
16-
railsContext: RailsContext;
1716
enforceRefetch?: boolean;
18-
createRSCPayloadKey: (componentName: string, componentProps: unknown) => string;
1917
};
2018

2119
const createFromFetch = async (fetchPromise: Promise<Response>) => {
@@ -40,11 +38,16 @@ const createFromFetch = async (fetchPromise: Promise<Response>) => {
4038
* This is used for client-side navigation or when rendering components
4139
* that weren't part of the initial server render.
4240
*
43-
* @param props - Object containing component name, props, and railsContext
41+
* @param props - Object containing component name and props
42+
* @param railsContext - The Rails context containing configuration
4443
* @returns A Promise resolving to the rendered React element
4544
* @throws Error if RSC payload generation URL path is not configured or network request fails
4645
*/
47-
const fetchRSC = ({ componentName, componentProps, railsContext }: ClientGetReactServerComponentProps) => {
46+
const fetchRSC = ({
47+
componentName,
48+
componentProps,
49+
railsContext,
50+
}: ClientGetReactServerComponentProps & { railsContext: RailsContext }) => {
4851
const { rscPayloadGenerationUrlPath } = railsContext;
4952

5053
if (!rscPayloadGenerationUrlPath) {
@@ -131,41 +134,48 @@ const createFromPreloadedPayloads = (payloads: string[]) => {
131134
};
132135

133136
/**
134-
* Fetches and renders a server component on the client side.
137+
* Creates a function that fetches and renders a server component on the client side.
135138
*
136-
* This function:
137-
* 1. Checks for embedded RSC payloads in window.REACT_ON_RAILS_RSC_PAYLOADS
139+
* This style of higher-order function is necessary as the function that gets server components
140+
* on server has different parameters than the function that gets them on client. The environment
141+
* dependent parameters (domNodeId, railsContext) are passed from the `wrapServerComponentRenderer`
142+
* function, while the environment agnostic parameters (componentName, componentProps, enforceRefetch)
143+
* are passed from the RSCProvider which is environment agnostic.
144+
*
145+
* The returned function:
146+
* 1. Checks for embedded RSC payloads in window.REACT_ON_RAILS_RSC_PAYLOADS using the domNodeId
138147
* 2. If found, uses the embedded payload to avoid an HTTP request
139148
* 3. If not found (during client navigation or dynamic rendering), fetches via HTTP
140149
* 4. Processes the RSC payload into React elements
141150
*
142151
* The embedded payload approach ensures optimal performance during initial page load,
143152
* while the HTTP fallback enables dynamic rendering after navigation.
144153
*
154+
* @param domNodeId - The DOM node ID to create a unique key for the RSC payload store
155+
* @param railsContext - Context for the current request, shared across all components
156+
* @returns A function that accepts RSC parameters and returns a Promise resolving to the rendered React element
157+
*
158+
* The returned function accepts:
145159
* @param componentName - Name of the server component to render
146160
* @param componentProps - Props to pass to the server component
147-
* @param railsContext - Context for the current request
148161
* @param enforceRefetch - Whether to enforce a refetch of the component
149-
* @returns A Promise resolving to the rendered React element
150162
*
151163
* @important This is an internal function. End users should not use this directly.
152164
* Instead, use the useRSC hook which provides getComponent and refetchComponent functions
153165
* for fetching or retrieving cached server components. For rendering server components,
154166
* consider using RSCRoute component which handles the rendering logic automatically.
155167
*/
156-
const getReactServerComponent = ({
157-
componentName,
158-
componentProps,
159-
railsContext,
160-
enforceRefetch = false,
161-
createRSCPayloadKey,
162-
}: ClientGetReactServerComponentProps) => {
163-
const componentKey = createRSCPayloadKey(componentName, componentProps);
164-
const payloads = window.REACT_ON_RAILS_RSC_PAYLOADS?.[componentKey];
165-
if (!enforceRefetch && payloads) {
166-
return createFromPreloadedPayloads(payloads);
167-
}
168-
return fetchRSC({ componentName, componentProps, railsContext, createRSCPayloadKey });
169-
};
168+
const getReactServerComponent =
169+
(domNodeId: string, railsContext: RailsContext) =>
170+
({ componentName, componentProps, enforceRefetch = false }: ClientGetReactServerComponentProps) => {
171+
const componentCacheKey = createRSCPayloadKey(componentName, componentProps);
172+
173+
const rscPayloadKey = `${componentCacheKey}-${domNodeId}`;
174+
const payloads = window.REACT_ON_RAILS_RSC_PAYLOADS?.[rscPayloadKey];
175+
if (!enforceRefetch && payloads) {
176+
return createFromPreloadedPayloads(payloads);
177+
}
178+
return fetchRSC({ componentName, componentProps, railsContext });
179+
};
170180

171181
export default getReactServerComponent;

node_package/src/getReactServerComponent.server.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { BundleManifest } from 'react-on-rails-rsc';
22
import { buildClientRenderer } from 'react-on-rails-rsc/client.node';
33
import transformRSCStream from './transformRSCNodeStream.ts';
44
import loadJsonFile from './loadJsonFile.ts';
5-
import { assertRailsContextWithServerStreamingCapabilities, RailsContext } from './types/index.ts';
5+
import type { RailsContextWithServerStreamingCapabilities } from './types/index.ts';
66

77
type GetReactServerComponentOnServerProps = {
88
componentName: string;
99
componentProps: unknown;
10-
railsContext: RailsContext;
1110
};
1211

1312
let clientRendererPromise: Promise<ReturnType<typeof buildClientRenderer>> | undefined;
@@ -37,9 +36,15 @@ const createFromReactOnRailsNodeStream = async (
3736
};
3837

3938
/**
40-
* Fetches and renders a server component on the server side.
39+
* Creates a function that fetches and renders a server component on the server side.
4140
*
42-
* This function:
41+
* This style of higher-order function is necessary as the function that gets server components
42+
* on server has different parameters than the function that gets them on client. The environment
43+
* dependent parameters (railsContext) are passed from the `wrapServerComponentRenderer`
44+
* function, while the environment agnostic parameters (componentName, componentProps) are
45+
* passed from the RSCProvider which is environment agnostic.
46+
*
47+
* The returned function:
4348
* 1. Validates the railsContext for required properties
4449
* 2. Creates an SSR manifest mapping server and client modules
4550
* 3. Gets the RSC payload stream via getRSCPayloadStream
@@ -49,31 +54,28 @@ const createFromReactOnRailsNodeStream = async (
4954
* - Used to render the server component
5055
* - Tracked so it can be embedded in the HTML response
5156
*
57+
* @param railsContext - Context for the current request with server streaming capabilities
58+
* @returns A function that accepts RSC parameters and returns a Promise resolving to the rendered React element
59+
*
60+
* The returned function accepts:
5261
* @param componentName - Name of the server component to render
5362
* @param componentProps - Props to pass to the server component
54-
* @param railsContext - Context for the current request
55-
* @param enforceRefetch - Whether to enforce a refetch of the component
56-
* @returns A Promise resolving to the rendered React element
5763
*
5864
* @important This is an internal function. End users should not use this directly.
5965
* Instead, use the useRSC hook which provides getComponent and refetchComponent functions
6066
* for fetching or retrieving cached server components. For rendering server components,
6167
* consider using RSCRoute component which handles the rendering logic automatically.
6268
*/
63-
const getReactServerComponent = async ({
64-
componentName,
65-
componentProps,
66-
railsContext,
67-
}: GetReactServerComponentOnServerProps) => {
68-
assertRailsContextWithServerStreamingCapabilities(railsContext);
69+
const getReactServerComponent =
70+
(railsContext: RailsContextWithServerStreamingCapabilities) =>
71+
async ({ componentName, componentProps }: GetReactServerComponentOnServerProps) => {
72+
const rscPayloadStream = await railsContext.getRSCPayloadStream(componentName, componentProps);
6973

70-
const rscPayloadStream = await railsContext.getRSCPayloadStream(componentName, componentProps);
71-
72-
return createFromReactOnRailsNodeStream(
73-
rscPayloadStream,
74-
railsContext.reactServerClientManifestFileName,
75-
railsContext.reactClientManifestFileName,
76-
);
77-
};
74+
return createFromReactOnRailsNodeStream(
75+
rscPayloadStream,
76+
railsContext.reactServerClientManifestFileName,
77+
railsContext.reactClientManifestFileName,
78+
);
79+
};
7880

7981
export default getReactServerComponent;

node_package/src/injectRSCPayload.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,24 @@ export default function injectRSCPayload(
210210

211211
rscRequestTracker.onRSCPayloadGenerated((streamInfo) => {
212212
const { stream, props, componentName } = streamInfo;
213-
const cacheKey = createRSCPayloadKey(componentName, props, domNodeId);
213+
const cacheKey = createRSCPayloadKey(componentName, props);
214+
const rscPayloadKey = `${cacheKey}-${domNodeId}`;
214215

215216
// CRITICAL TIMING: Initialize global array IMMEDIATELY when component requests RSC
216217
// This ensures the array exists before the component's HTML is rendered and sent.
217218
// Client-side hydration depends on this array being present in the page.
218219
//
219220
// The initialization script creates: (self.REACT_ON_RAILS_RSC_PAYLOADS||={})[cacheKey]||=[]
220221
// This creates a global array that the client-side RSCProvider monitors for new chunks.
221-
const initializationScript = createRSCPayloadInitializationScript(cacheKey);
222+
const initializationScript = createRSCPayloadInitializationScript(rscPayloadKey);
222223
rscInitializationBuffers.push(Buffer.from(initializationScript));
223224

224225
// Process RSC payload stream asynchronously
225226
rscPromises.push(
226227
(async () => {
227228
for await (const chunk of stream ?? []) {
228229
const decodedChunk = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
229-
const payloadScript = createRSCPayloadChunk(decodedChunk, cacheKey);
230+
const payloadScript = createRSCPayloadChunk(decodedChunk, rscPayloadKey);
230231
rscPayloadBuffers.push(Buffer.from(payloadScript));
231232
scheduleFlush();
232233
}

node_package/src/utils.ts

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,18 @@ const customFetch = (...args: Parameters<typeof fetch>) => {
1111
export { customFetch as fetch };
1212

1313
/**
14-
* Creates a unique cache key for RSC payloads to prevent collisions between component instances.
14+
* Creates a unique cache key for RSC payloads.
1515
*
1616
* This function generates cache keys that ensure:
1717
* 1. Different components have different keys
1818
* 2. Same components with different props have different keys
19-
* 3. Multiple instances of the same component on the same rails view have different keys
2019
*
2120
* @param componentName - Name of the React Server Component
2221
* @param componentProps - Props passed to the component (serialized to JSON)
23-
* @param domNodeId - DOM node ID of the parent react component rendered at the rails view (prevents instance collisions)
2422
* @returns A unique cache key string
25-
* @throws Error if domNodeId is missing, which could lead to cache collisions
2623
*/
27-
export const createRSCPayloadKey = (
28-
componentName: string,
29-
componentProps: unknown,
30-
domNodeId: string | undefined,
31-
) => {
32-
if (!domNodeId) {
33-
const errorMessage =
34-
'domNodeId is required when using React Server Components to ensure unique RSC payload caching ' +
35-
'and prevent conflicts between multiple instances of the same component at the same rails view. ' +
36-
'This could lead to incorrect hydration and component state issues.';
37-
38-
if (process.env.NODE_ENV === 'development') {
39-
throw new Error(`RSC Cache Key Error: ${errorMessage}`);
40-
} else {
41-
console.warn(`Warning: ${errorMessage}`);
42-
}
43-
}
44-
45-
return `${componentName}-${JSON.stringify(componentProps)}-${domNodeId}`;
24+
export const createRSCPayloadKey = (componentName: string, componentProps: unknown) => {
25+
return `${componentName}-${JSON.stringify(componentProps)}`;
4626
};
4727

4828
/**
@@ -60,3 +40,5 @@ export const wrapInNewPromise = <T>(promise: Promise<T>) => {
6040
void promise.catch(reject);
6141
});
6242
};
43+
44+
export const

node_package/src/wrapServerComponentRenderer/client.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ReactComponentOrRenderFunction, RenderFunction } from '../types/index.t
44
import isRenderFunction from '../isRenderFunction.ts';
55
import { ensureReactUseAvailable } from '../reactApis.cts';
66
import { createRSCProvider } from '../RSCProvider.tsx';
7-
import { createRSCPayloadKey } from '../utils.ts';
87
import getReactServerComponent from '../getReactServerComponent.client.ts';
98

109
ensureReactUseAvailable();
@@ -55,10 +54,7 @@ const wrapServerComponentRenderer = (componentOrRenderFunction: ReactComponentOr
5554
}
5655

5756
const RSCProvider = createRSCProvider({
58-
railsContext,
59-
getServerComponent: getReactServerComponent,
60-
createRSCPayloadKey: (componentName, componentProps) =>
61-
createRSCPayloadKey(componentName, componentProps, domNodeId),
57+
getServerComponent: getReactServerComponent(domNodeId, railsContext),
6258
});
6359

6460
const root = (

node_package/src/wrapServerComponentRenderer/server.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import getReactServerComponent from '../getReactServerComponent.server.ts';
44
import { createRSCProvider } from '../RSCProvider.tsx';
55
import isRenderFunction from '../isRenderFunction.ts';
66
import { assertRailsContextWithServerStreamingCapabilities } from '../types/index.ts';
7-
import { createRSCPayloadKey } from '../utils.ts';
87

98
/**
109
* Wraps a client component with the necessary RSC context and handling for server-side operations.
@@ -42,13 +41,7 @@ const wrapServerComponentRenderer = (componentOrRenderFunction: ReactComponentOr
4241
}
4342

4443
const RSCProvider = createRSCProvider({
45-
railsContext,
46-
getServerComponent: getReactServerComponent,
47-
// Server-side rendering processes each component in isolation during separate requests,
48-
// eliminating the possibility of cache key conflicts between component instances.
49-
// Therefore, we can safely use `server` as the domNodeId parameter.
50-
createRSCPayloadKey: (componentName, componentProps) =>
51-
createRSCPayloadKey(componentName, componentProps, 'server'),
44+
getServerComponent: getReactServerComponent(railsContext),
5245
});
5346

5447
return () => (

0 commit comments

Comments
 (0)