Skip to content

Commit 900f526

Browse files
Get rid of renderRequestId and use local trackers instead of global tracking (#1745)
1 parent b101358 commit 900f526

22 files changed

+482
-501
lines changed

lib/react_on_rails/helper.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,7 @@ def internal_react_component(react_component_name, options = {})
650650
"data-trace" => (render_options.trace ? true : nil),
651651
"data-dom-id" => render_options.dom_id,
652652
"data-store-dependencies" => render_options.store_dependencies&.to_json,
653-
"data-force-load" => (render_options.force_load ? true : nil),
654-
"data-render-request-id" => render_options.render_request_id)
653+
"data-force-load" => (render_options.force_load ? true : nil))
655654

656655
if render_options.force_load
657656
component_specification_tag.concat(

lib/react_on_rails/react_component/render_options.rb

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,9 @@ class RenderOptions
1515
def initialize(react_component_name: required("react_component_name"), options: required("options"))
1616
@react_component_name = react_component_name.camelize
1717
@options = options
18-
# The render_request_id serves as a unique identifier for each render request.
19-
# We cannot rely solely on dom_id, as it should be unique for each component on the page,
20-
# but the server can render the same page multiple times concurrently for different users.
21-
# Therefore, we need an additional unique identifier that can be used both on the client and server.
22-
# This ID can also be used to associate specific data with a particular rendered component
23-
# on either the server or client.
24-
# This ID is only present if RSC support is enabled because it's only used in that case.
25-
@render_request_id = self.class.generate_request_id if ReactOnRails::Utils.rsc_support_enabled?
2618
end
2719

28-
attr_reader :react_component_name, :render_request_id
20+
attr_reader :react_component_name
2921

3022
def throw_js_errors
3123
options.fetch(:throw_js_errors, false)

node_package/src/ClientSideRenderer.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,6 @@ class ComponentRenderer {
8383
const { domNodeId } = this;
8484
const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {};
8585
const trace = el.getAttribute('data-trace') === 'true';
86-
const renderRequestId = el.getAttribute('data-render-request-id');
87-
88-
// The renderRequestId is optional and only present when React Server Components (RSC) support is enabled.
89-
// When RSC is enabled, this ID helps track and associate server-rendered components with their client-side hydration.
90-
const componentSpecificRailsContext = renderRequestId
91-
? {
92-
...railsContext,
93-
componentSpecificMetadata: {
94-
renderRequestId,
95-
},
96-
}
97-
: railsContext;
9886

9987
try {
10088
const domNode = document.getElementById(domNodeId);
@@ -105,7 +93,7 @@ class ComponentRenderer {
10593
}
10694

10795
if (
108-
(await delegateToRenderer(componentObj, props, componentSpecificRailsContext, domNodeId, trace)) ||
96+
(await delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) ||
10997
// @ts-expect-error The state can change while awaiting delegateToRenderer
11098
this.state === 'unmounted'
11199
) {
@@ -120,7 +108,7 @@ class ComponentRenderer {
120108
props,
121109
domNodeId,
122110
trace,
123-
railsContext: componentSpecificRailsContext,
111+
railsContext,
124112
shouldHydrate,
125113
});
126114

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
type PostSSRHook = () => void;
2+
3+
/**
4+
* Post-SSR Hook Tracker - manages post-SSR hooks for a single request.
5+
*
6+
* This class provides a local alternative to the global hook management,
7+
* allowing each request to have its own isolated hook tracker without sharing state.
8+
*
9+
* The tracker ensures that:
10+
* - Hooks are executed exactly once when SSR ends
11+
* - No hooks can be added after SSR has completed
12+
* - Proper cleanup occurs to prevent memory leaks
13+
*/
14+
class PostSSRHookTracker {
15+
private hooks: PostSSRHook[] = [];
16+
17+
private hasSSREnded = false;
18+
19+
/**
20+
* Adds a hook to be executed when SSR ends for this request.
21+
*
22+
* @param hook - Function to call when SSR ends
23+
* @throws Error if called after SSR has already ended
24+
*/
25+
addPostSSRHook(hook: PostSSRHook): void {
26+
if (this.hasSSREnded) {
27+
console.error(
28+
'Cannot add post-SSR hook: SSR has already ended for this request. ' +
29+
'Hooks must be registered before or during the SSR process.',
30+
);
31+
return;
32+
}
33+
34+
this.hooks.push(hook);
35+
}
36+
37+
/**
38+
* Notifies all registered hooks that SSR has ended and clears the hook list.
39+
* This should be called exactly once when server-side rendering is complete.
40+
*
41+
* @throws Error if called multiple times
42+
*/
43+
notifySSREnd(): void {
44+
if (this.hasSSREnded) {
45+
console.warn('notifySSREnd() called multiple times. This may indicate a bug in the SSR lifecycle.');
46+
return;
47+
}
48+
49+
this.hasSSREnded = true;
50+
51+
// Execute all hooks and handle any errors gracefully
52+
this.hooks.forEach((hook, index) => {
53+
try {
54+
hook();
55+
} catch (error) {
56+
console.error(`Error executing post-SSR hook ${index}:`, error);
57+
}
58+
});
59+
60+
// Clear hooks to free memory
61+
this.hooks = [];
62+
}
63+
}
64+
65+
export default PostSSRHookTracker;

node_package/src/RSCPayloadGenerator.ts

Lines changed: 0 additions & 188 deletions
This file was deleted.

node_package/src/RSCProvider.tsx

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

65
type RSCContextType = {
@@ -31,31 +30,28 @@ const RSCContext = React.createContext<RSCContextType | undefined>(undefined);
3130
* for client-side rendering or 'react-on-rails/wrapServerComponentRenderer/server' for server-side rendering.
3231
*/
3332
export const createRSCProvider = ({
34-
railsContext,
3533
getServerComponent,
3634
}: {
37-
railsContext: RailsContextWithComponentSpecificMetadata;
38-
getServerComponent: typeof getReactServerComponent;
35+
getServerComponent: (props: ClientGetReactServerComponentProps) => Promise<React.ReactNode>;
3936
}) => {
4037
const fetchRSCPromises: Record<string, Promise<React.ReactNode>> = {};
4138

4239
const getComponent = (componentName: string, componentProps: unknown) => {
43-
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
40+
const key = createRSCPayloadKey(componentName, componentProps);
4441
if (key in fetchRSCPromises) {
4542
return fetchRSCPromises[key];
4643
}
4744

48-
const promise = getServerComponent({ componentName, componentProps, railsContext });
45+
const promise = getServerComponent({ componentName, componentProps });
4946
fetchRSCPromises[key] = promise;
5047
return promise;
5148
};
5249

5350
const refetchComponent = (componentName: string, componentProps: unknown) => {
54-
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
51+
const key = createRSCPayloadKey(componentName, componentProps);
5552
const promise = getServerComponent({
5653
componentName,
5754
componentProps,
58-
railsContext,
5955
enforceRefetch: true,
6056
});
6157
fetchRSCPromises[key] = promise;

0 commit comments

Comments
 (0)