Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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