Skip to content

Commit 7d6577c

Browse files
hydrate the component immediately when loaded and registered
1 parent d975912 commit 7d6577c

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
@@ -435,7 +435,7 @@ def build_react_component_result_for_server_rendered_string(
435435

436436
result_console_script = render_options.replay_console ? console_script : ""
437437
result = compose_react_component_html_with_spec_and_console(
438-
component_specification_tag, rendered_output, result_console_script
438+
component_specification_tag, rendered_output, result_console_script, render_options.dom_id
439439
)
440440

441441
prepend_render_rails_context(result)
@@ -501,12 +501,19 @@ def build_react_component_result_for_server_rendered_hash(
501501
)
502502
end
503503

504-
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
504+
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script, dom_id = nil)
505+
hydrate_script = dom_id.present? ? content_tag(:script, %(
506+
window.REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS.push('#{dom_id}');
507+
if (window.ReactOnRails) {
508+
window.ReactOnRails.renderOrHydrateLoadedComponents();
509+
}
510+
).html_safe) : ""
505511
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
506512
html_content = <<~HTML
507513
#{rendered_output}
508514
#{component_specification_tag}
509515
#{console_script}
516+
#{hydrate_script}
510517
HTML
511518
html_content.strip.html_safe
512519
end
@@ -518,10 +525,15 @@ def rails_context_if_not_already_rendered
518525

519526
@rendered_rails_context = true
520527

521-
content_tag(:script,
522-
json_safe_and_pretty(data).html_safe,
523-
type: "application/json",
524-
id: "js-react-on-rails-context")
528+
rails_context_tag = content_tag(:script,
529+
json_safe_and_pretty(data).html_safe,
530+
type: "application/json",
531+
id: "js-react-on-rails-context")
532+
rails_context_tag.concat(
533+
content_tag(:script, %(
534+
window.REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS = [];
535+
).html_safe)
536+
)
525537
end
526538

527539
# prepend the rails_context if not yet applied
@@ -553,6 +565,7 @@ def internal_react_component(react_component_name, options = {})
553565
json_safe_and_pretty(render_options.client_props).html_safe,
554566
type: "application/json",
555567
class: "js-react-on-rails-component",
568+
id: "js-react-on-rails-component-#{render_options.dom_id}",
556569
"data-component-name" => render_options.react_component_name,
557570
"data-trace" => (render_options.trace ? true : nil),
558571
"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
@@ -136,6 +136,10 @@ ctx.ReactOnRails = {
136136
ClientStartup.reactOnRailsPageLoaded();
137137
},
138138

139+
renderOrHydrateLoadedComponents(): void {
140+
ClientStartup.renderOrHydrateLoadedComponents();
141+
},
142+
139143
reactOnRailsComponentLoaded(domId: string): void {
140144
ClientStartup.reactOnRailsComponentLoaded(domId);
141145
},
@@ -240,6 +244,15 @@ ctx.ReactOnRails = {
240244
return ComponentRegistry.get(name);
241245
},
242246

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

node_package/src/clientStartup.ts

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

2527
namespace NodeJS {
2628
interface Global {
2729
ReactOnRails: ReactOnRailsType;
2830
roots: Root[];
31+
REACT_ON_RAILS_PENDING_COMPONENT_DOM_IDS?: string[];
2932
}
3033
}
3134
namespace Turbolinks {
@@ -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)