Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -650,8 +650,7 @@ def internal_react_component(react_component_name, options = {})
"data-trace" => (render_options.trace ? true : nil),
"data-dom-id" => render_options.dom_id,
"data-store-dependencies" => render_options.store_dependencies&.to_json,
"data-force-load" => (render_options.force_load ? true : nil),
"data-render-request-id" => render_options.render_request_id)
"data-force-load" => (render_options.force_load ? true : nil))

if render_options.force_load
component_specification_tag.concat(
Expand Down
10 changes: 1 addition & 9 deletions lib/react_on_rails/react_component/render_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,9 @@ class RenderOptions
def initialize(react_component_name: required("react_component_name"), options: required("options"))
@react_component_name = react_component_name.camelize
@options = options
# The render_request_id serves as a unique identifier for each render request.
# We cannot rely solely on dom_id, as it should be unique for each component on the page,
# but the server can render the same page multiple times concurrently for different users.
# Therefore, we need an additional unique identifier that can be used both on the client and server.
# This ID can also be used to associate specific data with a particular rendered component
# on either the server or client.
# This ID is only present if RSC support is enabled because it's only used in that case.
@render_request_id = self.class.generate_request_id if ReactOnRails::Utils.rsc_support_enabled?
end

attr_reader :react_component_name, :render_request_id
attr_reader :react_component_name

def throw_js_errors
options.fetch(:throw_js_errors, false)
Expand Down
16 changes: 2 additions & 14 deletions node_package/src/ClientSideRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,6 @@ class ComponentRenderer {
const { domNodeId } = this;
const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {};
const trace = el.getAttribute('data-trace') === 'true';
const renderRequestId = el.getAttribute('data-render-request-id');

// The renderRequestId is optional and only present when React Server Components (RSC) support is enabled.
// When RSC is enabled, this ID helps track and associate server-rendered components with their client-side hydration.
const componentSpecificRailsContext = renderRequestId
? {
...railsContext,
componentSpecificMetadata: {
renderRequestId,
},
}
: railsContext;

try {
const domNode = document.getElementById(domNodeId);
Expand All @@ -105,7 +93,7 @@ class ComponentRenderer {
}

if (
(await delegateToRenderer(componentObj, props, componentSpecificRailsContext, domNodeId, trace)) ||
(await delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) ||
// @ts-expect-error The state can change while awaiting delegateToRenderer
this.state === 'unmounted'
) {
Expand All @@ -120,7 +108,7 @@ class ComponentRenderer {
props,
domNodeId,
trace,
railsContext: componentSpecificRailsContext,
railsContext,
shouldHydrate,
});

Expand Down
65 changes: 65 additions & 0 deletions node_package/src/PostSSRHookTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
type PostSSRHook = () => void;

/**
* Post-SSR Hook Tracker - manages post-SSR hooks for a single request.
*
* This class provides a local alternative to the global hook management,
* allowing each request to have its own isolated hook tracker without sharing state.
*
* The tracker ensures that:
* - Hooks are executed exactly once when SSR ends
* - No hooks can be added after SSR has completed
* - Proper cleanup occurs to prevent memory leaks
*/
class PostSSRHookTracker {
private hooks: PostSSRHook[] = [];

private hasSSREnded = false;

/**
* Adds a hook to be executed when SSR ends for this request.
*
* @param hook - Function to call when SSR ends
* @throws Error if called after SSR has already ended
*/
addPostSSRHook(hook: PostSSRHook): void {
if (this.hasSSREnded) {
console.error(
'Cannot add post-SSR hook: SSR has already ended for this request. ' +
'Hooks must be registered before or during the SSR process.',
);
return;
}

this.hooks.push(hook);
}

/**
* Notifies all registered hooks that SSR has ended and clears the hook list.
* This should be called exactly once when server-side rendering is complete.
*
* @throws Error if called multiple times
*/
notifySSREnd(): void {
if (this.hasSSREnded) {
console.warn('notifySSREnd() called multiple times. This may indicate a bug in the SSR lifecycle.');
return;
}

this.hasSSREnded = true;

// Execute all hooks and handle any errors gracefully
this.hooks.forEach((hook, index) => {
try {
hook();
} catch (error) {
console.error(`Error executing post-SSR hook ${index}:`, error);
}
});

// Clear hooks to free memory
this.hooks = [];
}
}

export default PostSSRHookTracker;
188 changes: 0 additions & 188 deletions node_package/src/RSCPayloadGenerator.ts

This file was deleted.

14 changes: 5 additions & 9 deletions node_package/src/RSCProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import { RailsContextWithComponentSpecificMetadata } from './types/index.ts';
import getReactServerComponent from './getReactServerComponent.client.ts';
import type { ClientGetReactServerComponentProps } from './getReactServerComponent.client.ts';
import { createRSCPayloadKey } from './utils.ts';

type RSCContextType = {
Expand Down Expand Up @@ -31,31 +30,28 @@ const RSCContext = React.createContext<RSCContextType | undefined>(undefined);
* for client-side rendering or 'react-on-rails/wrapServerComponentRenderer/server' for server-side rendering.
*/
export const createRSCProvider = ({
railsContext,
getServerComponent,
}: {
railsContext: RailsContextWithComponentSpecificMetadata;
getServerComponent: typeof getReactServerComponent;
getServerComponent: (props: ClientGetReactServerComponentProps) => Promise<React.ReactNode>;
}) => {
const fetchRSCPromises: Record<string, Promise<React.ReactNode>> = {};

const getComponent = (componentName: string, componentProps: unknown) => {
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
const key = createRSCPayloadKey(componentName, componentProps);
if (key in fetchRSCPromises) {
return fetchRSCPromises[key];
}

const promise = getServerComponent({ componentName, componentProps, railsContext });
const promise = getServerComponent({ componentName, componentProps });
fetchRSCPromises[key] = promise;
return promise;
};

const refetchComponent = (componentName: string, componentProps: unknown) => {
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
const key = createRSCPayloadKey(componentName, componentProps);
const promise = getServerComponent({
componentName,
componentProps,
railsContext,
enforceRefetch: true,
});
fetchRSCPromises[key] = promise;
Expand Down
Loading
Loading