Skip to content

Commit 16078ef

Browse files
hydrate the component immediately when loaded and registered
1 parent cf15b2a commit 16078ef

File tree

5 files changed

+117
-33
lines changed

5 files changed

+117
-33
lines changed

lib/react_on_rails/helper.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def build_react_component_result_for_server_rendered_string(
444444

445445
result_console_script = render_options.replay_console ? console_script : ""
446446
result = compose_react_component_html_with_spec_and_console(
447-
component_specification_tag, rendered_output, result_console_script
447+
component_specification_tag, rendered_output, result_console_script, render_options.dom_id
448448
)
449449

450450
prepend_render_rails_context(result)
@@ -510,12 +510,19 @@ def build_react_component_result_for_server_rendered_hash(
510510
)
511511
end
512512

513-
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
513+
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script, dom_id = nil)
514+
hydrate_script = dom_id.present? ? content_tag(:script, %(
515+
window.REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS.push('#{dom_id}');
516+
if (window.ReactOnRails) {
517+
window.ReactOnRails.renderOrHydrateLoadedComponents();
518+
}
519+
).html_safe) : ""
514520
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
515521
html_content = <<~HTML
516522
#{rendered_output}
517523
#{component_specification_tag}
518524
#{console_script}
525+
#{hydrate_script}
519526
HTML
520527
html_content.strip.html_safe
521528
end
@@ -527,10 +534,15 @@ def rails_context_if_not_already_rendered
527534

528535
@rendered_rails_context = true
529536

530-
content_tag(:script,
531-
json_safe_and_pretty(data).html_safe,
532-
type: "application/json",
533-
id: "js-react-on-rails-context")
537+
rails_context_tag = content_tag(:script,
538+
json_safe_and_pretty(data).html_safe,
539+
type: "application/json",
540+
id: "js-react-on-rails-context")
541+
rails_context_tag.concat(
542+
content_tag(:script, %(
543+
window.REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS = [];
544+
).html_safe)
545+
)
534546
end
535547

536548
# prepend the rails_context if not yet applied
@@ -555,6 +567,7 @@ def internal_react_component(react_component_name, options = {})
555567
json_safe_and_pretty(render_options.client_props).html_safe,
556568
type: "application/json",
557569
class: "js-react-on-rails-component",
570+
id: "js-react-on-rails-component-#{render_options.dom_id}",
558571
"data-component-name" => render_options.react_component_name,
559572
"data-trace" => (render_options.trace ? true : nil),
560573
"data-dom-id" => render_options.dom_id)

node_package/src/ComponentRegistry.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,31 @@ import type { RegisteredComponent, ReactComponentOrRenderFunction, RenderFunctio
22
import isRenderFunction from './isRenderFunction';
33

44
const registeredComponents = new Map<string, RegisteredComponent>();
5+
const registrationCallbacks = new Map<string, Array<(component: RegisteredComponent) => void>>();
56

67
export default {
8+
/**
9+
* Register a callback to be called when a specific component is registered
10+
* @param componentName Name of the component to watch for
11+
* @param callback Function called with the component details when registered
12+
*/
13+
onComponentRegistered(
14+
componentName: string,
15+
callback: (component: RegisteredComponent) => void
16+
): void {
17+
// If component is already registered, schedule callback
18+
const existingComponent = registeredComponents.get(componentName);
19+
if (existingComponent) {
20+
setTimeout(() => callback(existingComponent), 0);
21+
return;
22+
}
23+
24+
// Store callback for future registration
25+
const callbacks = registrationCallbacks.get(componentName) || [];
26+
callbacks.push(callback);
27+
registrationCallbacks.set(componentName, callbacks);
28+
},
29+
730
/**
831
* @param components { component1: component1, component2: component2, etc. }
932
*/
@@ -21,12 +44,19 @@ export default {
2144
const renderFunction = isRenderFunction(component);
2245
const isRenderer = renderFunction && (component as RenderFunction).length === 3;
2346

24-
registeredComponents.set(name, {
47+
const registeredComponent = {
2548
name,
2649
component,
2750
renderFunction,
2851
isRenderer,
52+
};
53+
registeredComponents.set(name, registeredComponent);
54+
55+
const callbacks = registrationCallbacks.get(name) || [];
56+
callbacks.forEach(callback => {
57+
setTimeout(() => callback(registeredComponent), 0);
2958
});
59+
registrationCallbacks.delete(name);
3060
});
3161
},
3262

@@ -45,6 +75,12 @@ export default {
4575
Registered component names include [ ${keys} ]. Maybe you forgot to register the component?`);
4676
},
4777

78+
async getOrWaitForComponent(name: string): Promise<RegisteredComponent> {
79+
return new Promise((resolve) => {
80+
this.onComponentRegistered(name, resolve);
81+
});
82+
},
83+
4884
/**
4985
* Get a Map containing all registered components. Useful for debugging.
5086
* @returns Map where key is the component name and values are the

node_package/src/ReactOnRails.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ ctx.ReactOnRails = {
134134
ClientStartup.reactOnRailsPageLoaded();
135135
},
136136

137+
renderOrHydrateLoadedComponents(): void {
138+
ClientStartup.renderOrHydrateLoadedComponents();
139+
},
140+
137141
reactOnRailsComponentLoaded(domId: string): void {
138142
ClientStartup.reactOnRailsComponentLoaded(domId);
139143
},
@@ -238,6 +242,15 @@ ctx.ReactOnRails = {
238242
return ComponentRegistry.get(name);
239243
},
240244

245+
/**
246+
* Get the component that you registered, or wait for it to be registered
247+
* @param name
248+
* @returns {name, component, renderFunction, isRenderer}
249+
*/
250+
getOrWaitForComponent(name: string): Promise<RegisteredComponent> {
251+
return ComponentRegistry.getOrWaitForComponent(name);
252+
},
253+
241254
/**
242255
* Used by server rendering by Rails
243256
* @param options

node_package/src/clientStartup.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ declare global {
2121
ReactOnRails: ReactOnRailsType;
2222
__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__?: boolean;
2323
roots: Root[];
24+
REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS?: string[];
25+
REACT_ON_RAILS_UNMOUNTED_BEFORE?: boolean;
2426
}
2527

2628
namespace globalThis {
2729
/* eslint-disable no-var,vars-on-top */
2830
var ReactOnRails: ReactOnRailsType;
2931
var roots: Root[];
32+
var REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS: string[] | undefined;
3033
/* eslint-enable no-var,vars-on-top */
3134
}
3235

@@ -134,7 +137,7 @@ function domNodeIdForEl(el: Element): string {
134137
* Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or
135138
* delegates to a renderer registered by the user.
136139
*/
137-
function render(el: Element, context: Context, railsContext: RailsContext): void {
140+
async function render(el: Element, context: Context, railsContext: RailsContext): Promise<void> {
138141
// This must match lib/react_on_rails/helper.rb
139142
const name = el.getAttribute('data-component-name') || '';
140143
const domNodeId = domNodeIdForEl(el);
@@ -144,7 +147,7 @@ function render(el: Element, context: Context, railsContext: RailsContext): void
144147
try {
145148
const domNode = document.getElementById(domNodeId);
146149
if (domNode) {
147-
const componentObj = context.ReactOnRails.getComponent(name);
150+
const componentObj = await context.ReactOnRails.getOrWaitForComponent(name);
148151
if (delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) {
149152
return;
150153
}
@@ -180,13 +183,6 @@ You should return a React.Component always for the client side entry point.`);
180183
}
181184
}
182185

183-
function forEachReactOnRailsComponentRender(context: Context, railsContext: RailsContext): void {
184-
const els = reactOnRailsHtmlElements();
185-
for (let i = 0; i < els.length; i += 1) {
186-
render(els[i], context, railsContext);
187-
}
188-
}
189-
190186
function parseRailsContext(): RailsContext | null {
191187
const el = document.getElementById('js-react-on-rails-context');
192188
if (!el) {
@@ -202,39 +198,62 @@ function parseRailsContext(): RailsContext | null {
202198
return JSON.parse(el.textContent);
203199
}
204200

201+
function getContextAndRailsContext(): { context: Context; railsContext: RailsContext | null } {
202+
const railsContext = parseRailsContext();
203+
const context = findContext();
204+
205+
if (railsContext && supportsRootApi && !context.roots) {
206+
context.roots = [];
207+
}
208+
209+
return { context, railsContext };
210+
}
211+
205212
export function reactOnRailsPageLoaded(): void {
206213
debugTurbolinks('reactOnRailsPageLoaded');
207214

208-
const railsContext = parseRailsContext();
209-
215+
const { context, railsContext } = getContextAndRailsContext();
216+
210217
// If no react on rails components
211218
if (!railsContext) return;
212219

213-
const context = findContext();
214-
if (supportsRootApi) {
215-
context.roots = [];
216-
}
217220
forEachStore(context, railsContext);
218-
forEachReactOnRailsComponentRender(context, railsContext);
219221
}
220222

221-
export function reactOnRailsComponentLoaded(domId: string): void {
222-
debugTurbolinks(`reactOnRailsComponentLoaded ${domId}`);
223+
async function renderUsingDomId(domId: string, context: Context, railsContext: RailsContext) {
224+
const el = document.querySelector(`[data-dom-id=${domId}]`);
225+
if (!el) return;
223226

224-
const railsContext = parseRailsContext();
227+
await render(el, context, railsContext);
228+
}
225229

230+
export async function renderOrHydrateLoadedComponents(): Promise<void> {
231+
debugTurbolinks('renderOrHydrateLoadedComponents');
232+
233+
const { context, railsContext } = getContextAndRailsContext();
234+
226235
// If no react on rails components
227236
if (!railsContext) return;
228237

229-
const context = findContext();
230-
if (supportsRootApi) {
231-
context.roots = [];
232-
}
238+
// copy and clear the pending dom ids, so they don't get processed again
239+
const pendingDomIds = context.REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS ?? [];
240+
context.REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS = [];
241+
await Promise.all(
242+
pendingDomIds.map(async (domId) => {
243+
await renderUsingDomId(domId, context, railsContext);
244+
})
245+
);
246+
}
233247

234-
const el = document.querySelector(`[data-dom-id=${domId}]`);
235-
if (!el) return;
248+
export async function reactOnRailsComponentLoaded(domId: string): Promise<void> {
249+
debugTurbolinks(`reactOnRailsComponentLoaded ${domId}`);
250+
251+
const { context, railsContext } = getContextAndRailsContext();
252+
253+
// If no react on rails components
254+
if (!railsContext) return;
236255

237-
render(el, context, railsContext);
256+
await renderUsingDomId(domId, context, railsContext);
238257
}
239258

240259
function unmount(el: Element): void {
@@ -333,5 +352,6 @@ export function clientStartup(context: Context): void {
333352
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
334353
context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__ = true;
335354

355+
console.log('clientStartup');
336356
onPageReady(renderInit);
337357
}

node_package/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export interface ReactOnRails {
158158
setOptions(newOptions: {traceTurbolinks: boolean}): void;
159159
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType;
160160
reactOnRailsPageLoaded(): void;
161+
renderOrHydrateLoadedComponents(): void;
161162
reactOnRailsComponentLoaded(domId: string): void;
162163
authenticityToken(): string | null;
163164
authenticityHeaders(otherHeaders: { [id: string]: string }): AuthenticityHeaders;
@@ -169,6 +170,7 @@ export interface ReactOnRails {
169170
name: string, props: Record<string, string>, domNodeId: string, hydrate: boolean
170171
): RenderReturnType;
171172
getComponent(name: string): RegisteredComponent;
173+
getOrWaitForComponent(name: string): Promise<RegisteredComponent>;
172174
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>;
173175
streamServerRenderedReactComponent(options: RenderParams): Readable;
174176
serverRenderRSCReactComponent(options: RenderParams): PassThrough;

0 commit comments

Comments
 (0)