Skip to content

Commit 0e4b561

Browse files
refactor: rename rscPayloadGenerationUrl to rscPayloadGenerationUrlPath and enhance type safety in RailsContext
1 parent e1949c4 commit 0e4b561

File tree

11 files changed

+83
-46
lines changed

11 files changed

+83
-46
lines changed

lib/react_on_rails/helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ def rails_context(server_side: true)
381381
if ReactOnRails::Utils.react_on_rails_pro?
382382
result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version
383383

384-
result[:rscPayloadGenerationUrl] = rsc_url if ReactOnRailsPro.configuration.enable_rsc_support
384+
result[:rscPayloadGenerationUrlPath] = rsc_url if ReactOnRailsPro.configuration.enable_rsc_support
385385
end
386386

387387
if defined?(request) && request.present?

lib/react_on_rails/utils.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def self.react_server_client_manifest_file_path
130130
asset_name = ReactOnRails.configuration.react_server_client_manifest_file
131131
if asset_name.nil?
132132
raise ReactOnRails::Error,
133-
"react_server_client_manifest_file is nil, ensure to set it in your configuration"
133+
"react_server_client_manifest_file is nil, ensure it is set in your configuration"
134134
end
135135

136136
@react_server_manifest_path = File.join(generated_assets_full_path, asset_name)

node_package/src/ClientSideRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class ComponentRenderer {
8686
const renderRequestId = el.getAttribute('data-render-request-id');
8787

8888
if (!renderRequestId) {
89-
console.error(`renderRequestId is missing for ${name} in dom node with id: ${domNodeId}`);
89+
console.error(`renderRequestId is missing for component ${name} in the DOM node with id ${domNodeId}`);
9090
}
9191

9292
const componentSpecificRailsContext = {

node_package/src/RSCPayloadGenerator.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ declare global {
1515

1616
const mapRailsContextToRSCPayloadStreams = new Map<string, RSCPayloadStreamInfo[]>();
1717

18-
const rscPayloadCallbacks = new Map<string, Array<RSCPayloadCallback>>();
18+
const rscPayloadCallbacks = new Map<string, RSCPayloadCallback[]>();
1919

2020
/**
2121
* Registers a callback to be executed when RSC payloads are generated.
@@ -36,13 +36,18 @@ export const onRSCPayloadGenerated = (
3636
callback: RSCPayloadCallback,
3737
) => {
3838
const { renderRequestId } = railsContext.componentSpecificMetadata;
39-
const callbacks = rscPayloadCallbacks.get(renderRequestId) || [];
40-
callbacks.push(callback);
41-
rscPayloadCallbacks.set(renderRequestId, callbacks);
39+
const callbacks = rscPayloadCallbacks.get(renderRequestId);
40+
if (callbacks) {
41+
callbacks.push(callback);
42+
} else {
43+
rscPayloadCallbacks.set(renderRequestId, [callback]);
44+
}
4245

4346
// Call callback for any existing streams for this context
44-
const existingStreams = mapRailsContextToRSCPayloadStreams.get(renderRequestId) || [];
45-
existingStreams.forEach((streamInfo) => callback(streamInfo));
47+
const existingStreams = mapRailsContextToRSCPayloadStreams.get(renderRequestId);
48+
if (existingStreams) {
49+
existingStreams.forEach((streamInfo) => callback(streamInfo));
50+
}
4651
};
4752

4853
/**
@@ -76,7 +81,10 @@ export const getRSCPayloadStream = async (
7681

7782
const { renderRequestId } = railsContext.componentSpecificMetadata;
7883
const stream = await generateRSCPayload(componentName, props, railsContext);
79-
const streams = mapRailsContextToRSCPayloadStreams.get(renderRequestId) ?? [];
84+
// Tee stream to allow for multiple consumers:
85+
// 1. stream1 - Used by React's runtime to perform server-side rendering
86+
// 2. stream2 - Used by react-on-rails to embed the RSC payloads
87+
// into the HTML stream for client-side hydration
8088
const stream1 = new PassThrough();
8189
stream.pipe(stream1);
8290
const stream2 = new PassThrough();
@@ -87,13 +95,19 @@ export const getRSCPayloadStream = async (
8795
props,
8896
stream: stream2,
8997
};
90-
streams.push(streamInfo);
91-
mapRailsContextToRSCPayloadStreams.set(renderRequestId, streams);
98+
const streams = mapRailsContextToRSCPayloadStreams.get(renderRequestId);
99+
if (streams) {
100+
streams.push(streamInfo);
101+
} else {
102+
mapRailsContextToRSCPayloadStreams.set(renderRequestId, [streamInfo]);
103+
}
92104

93105
// Notify callbacks about the new stream in a sync manner to maintain proper hydration timing
94106
// as described in the comment above onRSCPayloadGenerated
95-
const callbacks = rscPayloadCallbacks.get(renderRequestId) || [];
96-
callbacks.forEach((callback) => callback(streamInfo));
107+
const callbacks = rscPayloadCallbacks.get(renderRequestId);
108+
if (callbacks) {
109+
callbacks.forEach((callback) => callback(streamInfo));
110+
}
97111

98112
return stream1;
99113
};

node_package/src/RSCProvider.tsx

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

56
type RSCContextType = {
67
getCachedComponent: (componentName: string, componentProps: unknown) => React.ReactNode;
@@ -33,18 +34,14 @@ export const createRSCProvider = ({
3334
railsContext,
3435
getServerComponent,
3536
}: {
36-
railsContext: RailsContext;
37+
railsContext: RailsContextWithComponentSpecificMetadata;
3738
getServerComponent: typeof getReactServerComponent;
3839
}) => {
3940
const cachedComponents: Record<string, React.ReactNode> = {};
4041
const fetchRSCPromises: Record<string, Promise<React.ReactNode>> = {};
4142

42-
const generateCacheKey = (componentName: string, componentProps: unknown) => {
43-
return `${componentName}-${JSON.stringify(componentProps)}-${railsContext.componentSpecificMetadata?.renderRequestId}`;
44-
};
45-
4643
const getCachedComponent = (componentName: string, componentProps: unknown) => {
47-
const key = generateCacheKey(componentName, componentProps);
44+
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
4845
return cachedComponents[key];
4946
};
5047

@@ -53,7 +50,7 @@ export const createRSCProvider = ({
5350
if (cachedComponent) {
5451
return cachedComponent;
5552
}
56-
const key = generateCacheKey(componentName, componentProps);
53+
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
5754
if (key in fetchRSCPromises) {
5855
return fetchRSCPromises[key];
5956
}
@@ -66,9 +63,10 @@ export const createRSCProvider = ({
6663
return promise;
6764
};
6865

66+
const contextValue = { getCachedComponent, getComponent };
67+
6968
return ({ children }: { children: React.ReactNode }) => {
70-
// eslint-disable-next-line react/jsx-no-constructed-context-values
71-
return <RSCContext.Provider value={{ getCachedComponent, getComponent }}>{children}</RSCContext.Provider>;
69+
return <RSCContext.Provider value={contextValue}>{children}</RSCContext.Provider>;
7270
};
7371
};
7472

node_package/src/RSCRoute.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ export type RSCRouteProps = {
3030

3131
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps) => {
3232
const { getComponent, getCachedComponent } = useRSC();
33-
let component = getCachedComponent(componentName, componentProps);
34-
if (!component) {
35-
component = React.use(getComponent(componentName, componentProps));
36-
}
33+
const component =
34+
getCachedComponent(componentName, componentProps) ??
35+
React.use(getComponent(componentName, componentProps));
3736
return component;
3837
};
3938

node_package/src/getReactServerComponent.client.ts

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

77
declare global {
88
interface Window {
@@ -42,8 +42,8 @@ const createFromFetch = async (fetchPromise: Promise<Response>) => {
4242
*/
4343
const fetchRSC = ({ componentName, componentProps, railsContext }: ClientGetReactServerComponentProps) => {
4444
const propsString = JSON.stringify(componentProps);
45-
const { rscPayloadGenerationUrl } = railsContext;
46-
const strippedUrlPath = rscPayloadGenerationUrl?.replace(/^\/|\/$/g, '');
45+
const { rscPayloadGenerationUrlPath } = railsContext;
46+
const strippedUrlPath = rscPayloadGenerationUrlPath?.replace(/^\/|\/$/g, '');
4747
return createFromFetch(fetch(`/${strippedUrlPath}/${componentName}?props=${propsString}`));
4848
};
4949

@@ -126,7 +126,8 @@ const getReactServerComponent = ({
126126
componentProps,
127127
railsContext,
128128
}: ClientGetReactServerComponentProps) => {
129-
const componentKey = `${componentName}-${JSON.stringify(componentProps)}-${railsContext.componentSpecificMetadata?.renderRequestId}`;
129+
assertRailsContextWithComponentSpecificMetadata(railsContext);
130+
const componentKey = createRSCPayloadKey(componentName, componentProps, railsContext);
130131
const payloads = window.REACT_ON_RAILS_RSC_PAYLOADS?.[componentKey];
131132
if (payloads) {
132133
return createFromPreloadedPayloads(payloads);

node_package/src/types/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type RailsContext = {
3434
pathname: string;
3535
search: string | null;
3636
httpAcceptLanguage: string;
37-
rscPayloadGenerationUrl?: string;
37+
rscPayloadGenerationUrlPath?: string;
3838
componentSpecificMetadata?: {
3939
// The renderRequestId serves as a unique identifier for each render request.
4040
// We cannot rely solely on nodeDomId, as it should be unique for each component on the page,
@@ -62,14 +62,29 @@ export type RailsContext = {
6262
}
6363
);
6464

65-
export type RailsContextWithServerComponentCapabilities = RailsContext & {
65+
export type RailsContextWithComponentSpecificMetadata = RailsContext & {
66+
componentSpecificMetadata: {
67+
renderRequestId: string;
68+
};
69+
};
70+
71+
export type RailsContextWithServerComponentCapabilities = RailsContextWithComponentSpecificMetadata & {
6672
serverSide: true;
6773
serverSideRSCPayloadParameters: unknown;
6874
reactClientManifestFileName: string;
6975
reactServerClientManifestFileName: string;
70-
componentSpecificMetadata: {
71-
renderRequestId: string;
72-
};
76+
};
77+
78+
export const assertRailsContextWithComponentSpecificMetadata: (
79+
context: RailsContext | undefined,
80+
) => asserts context is RailsContextWithComponentSpecificMetadata = (
81+
context: RailsContext | undefined,
82+
): asserts context is RailsContextWithComponentSpecificMetadata => {
83+
if (!context || !('componentSpecificMetadata' in context)) {
84+
throw new Error(
85+
'Rails context does not have component specific metadata. Please ensure you are using a compatible version of react_on_rails_pro',
86+
);
87+
}
7388
};
7489

7590
export const assertRailsContextWithServerComponentCapabilities: (

node_package/src/utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { RailsContextWithComponentSpecificMetadata } from './types/index.ts';
2+
13
// Override the fetch function to make it easier to test
24
// The default fetch implementation in jest returns Node's Readable stream
35
// In jest.setup.js, we configure this fetch to return a web-standard ReadableStream instead,
@@ -8,5 +10,12 @@ const customFetch = (...args: Parameters<typeof fetch>) => {
810
return res;
911
};
1012

11-
// eslint-disable-next-line import/prefer-default-export
1213
export { customFetch as fetch };
14+
15+
export const createRSCPayloadKey = (
16+
componentName: string,
17+
componentProps: unknown,
18+
railsContext: RailsContextWithComponentSpecificMetadata,
19+
) => {
20+
return `${componentName}-${JSON.stringify(componentProps)}-${railsContext.componentSpecificMetadata.renderRequestId}`;
21+
};

node_package/src/wrapServerComponentRenderer/client.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import * as React from 'react';
22
import * as ReactDOMClient from 'react-dom/client';
3-
import { ReactComponentOrRenderFunction, RenderFunction } from '../types/index.ts';
3+
import {
4+
ReactComponentOrRenderFunction,
5+
RenderFunction,
6+
assertRailsContextWithComponentSpecificMetadata,
7+
} from '../types/index.ts';
48
import isRenderFunction from '../isRenderFunction.ts';
59
import { ensureReactUseAvailable } from '../reactApis.cts';
610
import { createRSCProvider } from '../RSCProvider.tsx';
@@ -41,9 +45,7 @@ const wrapServerComponentRenderer = (componentOrRenderFunction: ReactComponentOr
4145
throw new Error('wrapServerComponentRenderer: component is not a function');
4246
}
4347

44-
if (!railsContext) {
45-
throw new Error('RSCClientRoot: No railsContext provided');
46-
}
48+
assertRailsContextWithComponentSpecificMetadata(railsContext);
4749

4850
const RSCProvider = createRSCProvider({
4951
railsContext,

0 commit comments

Comments
 (0)