react-on-rails 17.0.0-rc.1 → 17.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ClientRenderer.js +152 -32
- package/lib/ComponentRegistry.d.ts +5 -4
- package/lib/base/client.d.ts +5 -4
- package/lib/base/client.js +3 -2
- package/lib/capabilities/core.d.ts +11 -9
- package/lib/capabilities/core.js +3 -2
- package/lib/componentRegistrationMetric.d.ts +8 -0
- package/lib/componentRegistrationMetric.js +7 -0
- package/lib/createReactOutput.js +41 -2
- package/lib/isRenderFunction.d.ts +6 -4
- package/lib/isRenderFunction.js +9 -5
- package/lib/rendererTeardown.d.ts +3 -0
- package/lib/rendererTeardown.js +9 -0
- package/lib/serverRenderUtils.d.ts +2 -2
- package/lib/types/index.d.ts +104 -14
- package/package.json +2 -1
package/lib/ClientRenderer.js
CHANGED
|
@@ -6,9 +6,56 @@ import { getRailsContext } from "./context.js";
|
|
|
6
6
|
import { isServerRenderHash } from "./isServerRenderResult.js";
|
|
7
7
|
import { onPageUnloaded } from "./pageLifecycle.js";
|
|
8
8
|
import { supportsRootApi, unmountComponentAtNode } from "./reactApis.cjs";
|
|
9
|
+
import { isRendererTeardownResult } from "./rendererTeardown.js";
|
|
9
10
|
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
|
|
11
|
+
/** Narrows an unknown value to a thenable (has a callable `.then`) without assuming a native Promise. */
|
|
12
|
+
function isThenable(value) {
|
|
13
|
+
return (value != null &&
|
|
14
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
15
|
+
typeof value.then === 'function');
|
|
16
|
+
}
|
|
10
17
|
// Track all rendered roots for cleanup
|
|
11
18
|
const renderedRoots = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* Invokes a renderer teardown, swallowing async rejections so a failing teardown cannot produce an
|
|
21
|
+
* unhandled promise rejection. Synchronous throws propagate to the caller's try/catch. `domNodeId`
|
|
22
|
+
* is included in the log so a failure can be traced to its mount.
|
|
23
|
+
* MUST SYNC: A sibling helper exists in packages/react-on-rails-pro/src/ClientSideRenderer.ts. If you
|
|
24
|
+
* change the error-handling logic or log format here, update that copy too.
|
|
25
|
+
*/
|
|
26
|
+
function invokeRendererTeardown(teardown, domNodeId) {
|
|
27
|
+
if (!teardown)
|
|
28
|
+
return;
|
|
29
|
+
const maybePromise = teardown();
|
|
30
|
+
if (isThenable(maybePromise)) {
|
|
31
|
+
// Detect a thenable with `.then` (Promises/A+) but swallow the rejection via
|
|
32
|
+
// `Promise.resolve(...).catch(...)`: a non-native thenable may lack `.catch`, so calling it
|
|
33
|
+
// directly could itself throw or leave the rejection unhandled. This keeps a failing async
|
|
34
|
+
// teardown from surfacing as an unhandled promise rejection.
|
|
35
|
+
Promise.resolve(maybePromise).catch((error) => {
|
|
36
|
+
console.error(`Error in renderer teardown for dom node "${domNodeId}":`, error);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Tears down a single tracked entry: runs the renderer's teardown, or unmounts the React root.
|
|
42
|
+
* Synchronous errors are not caught here; callers wrap this in try/catch so one failure does not
|
|
43
|
+
* abort cleanup of the remaining entries.
|
|
44
|
+
*/
|
|
45
|
+
function teardownEntry(entry, domNodeId) {
|
|
46
|
+
if (entry.kind === 'renderer') {
|
|
47
|
+
invokeRendererTeardown(entry.teardown, domNodeId);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (supportsRootApi && entry.root && typeof entry.root === 'object' && 'unmount' in entry.root) {
|
|
51
|
+
// React 18+ Root API
|
|
52
|
+
entry.root.unmount();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// React 16-17 legacy API
|
|
56
|
+
unmountComponentAtNode(entry.domNode);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
12
59
|
function initializeStore(el, railsContext) {
|
|
13
60
|
const name = el.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
|
|
14
61
|
const props = el.textContent !== null ? JSON.parse(el.textContent) : {};
|
|
@@ -32,11 +79,85 @@ function delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)
|
|
|
32
79
|
console.log(`\
|
|
33
80
|
DELEGATING TO RENDERER ${name} for dom node with id: ${domNodeId} with props, railsContext:`, props, railsContext);
|
|
34
81
|
}
|
|
35
|
-
// Call the renderer function with the expected signature
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
// Call the renderer function with the expected signature. A renderer owns its own mount and may
|
|
83
|
+
// return nothing, a teardown wrapper, or a promise resolving to one. `component` is the registered
|
|
84
|
+
// component union, so `as RendererFunction` is a runtime-invariant assertion guarded by
|
|
85
|
+
// `isRenderer` (the registry only sets it for a 3-arg render function), not a structural
|
|
86
|
+
// narrowing.
|
|
87
|
+
if (typeof component !== 'function') {
|
|
88
|
+
throw new Error(`Registered renderer "${name}" must be a function.`);
|
|
89
|
+
}
|
|
90
|
+
const result = component(props, railsContext, domNodeId);
|
|
91
|
+
return { delegated: true, result };
|
|
92
|
+
}
|
|
93
|
+
return { delegated: false };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Records a renderer-function mount and captures its optional teardown. The renderer may return a
|
|
97
|
+
* teardown wrapper synchronously, or a promise resolving to one (async renderers). The entry is
|
|
98
|
+
* stored immediately so the replaced-node path can find it even before an async teardown resolves.
|
|
99
|
+
*
|
|
100
|
+
* Known limitation (core package): if the mount is unmounted or its node is replaced *before* an
|
|
101
|
+
* async renderer resolves its teardown, the still-pending teardown is dropped and that one mount
|
|
102
|
+
* leaks — the resolved teardown is discarded because the entry is no longer the active mount for
|
|
103
|
+
* this id (the `renderedRoots.get(domNodeId) === entry` guard below). The drop is an expected,
|
|
104
|
+
* documented core limitation, but it still leaves a renderer-owned mount uncleaned, so it is logged
|
|
105
|
+
* with `console.error` rather than silently ignored. Synchronous teardowns are unaffected. The Pro
|
|
106
|
+
* client renderer (`react-on-rails-pro`) awaits the renderer and re-checks the unmount state, so it
|
|
107
|
+
* runs the teardown even when a navigation races the resolve; prefer Pro if you depend on async
|
|
108
|
+
* renderer teardowns surviving fast navigations.
|
|
109
|
+
*
|
|
110
|
+
* Renderer results that ultimately do not include a teardown wrapper are left untracked:
|
|
111
|
+
* synchronous no-wrapper returns are not stored, and async results that resolve without a wrapper use
|
|
112
|
+
* only a temporary placeholder until the promise settles. Before this cleanup contract existed,
|
|
113
|
+
* renderer-owned mounts were never tracked, so repeated page-loaded calls re-invoked those legacy
|
|
114
|
+
* renderers; preserving that behavior keeps "return nothing" backward compatible.
|
|
115
|
+
*/
|
|
116
|
+
function trackRendererMount(domNodeId, domNode, result) {
|
|
117
|
+
if (isRendererTeardownResult(result)) {
|
|
118
|
+
renderedRoots.set(domNodeId, { kind: 'renderer', domNode, teardown: result.teardown });
|
|
119
|
+
}
|
|
120
|
+
else if (isThenable(result)) {
|
|
121
|
+
const entry = { kind: 'renderer', domNode, teardown: undefined };
|
|
122
|
+
renderedRoots.set(domNodeId, entry);
|
|
123
|
+
Promise.resolve(result)
|
|
124
|
+
.then((resolved) => {
|
|
125
|
+
if (!isRendererTeardownResult(resolved)) {
|
|
126
|
+
if (renderedRoots.get(domNodeId) === entry) {
|
|
127
|
+
renderedRoots.delete(domNodeId);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Only attach if this exact entry is still the active mount for this id.
|
|
132
|
+
if (renderedRoots.get(domNodeId) === entry) {
|
|
133
|
+
entry.teardown = resolved.teardown;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// The mount was unmounted or its node replaced before this async teardown resolved, so the
|
|
137
|
+
// entry is no longer the active mount and the teardown can't be attached — it is dropped
|
|
138
|
+
// and that one mount may leak on cleanup. This is the expected, documented best-effort core
|
|
139
|
+
// limitation, but the consequence is still a leak, so log it as an error. Pro avoids this
|
|
140
|
+
// race entirely.
|
|
141
|
+
console.error(`[react-on-rails] Renderer teardown for dom node "${domNodeId}" resolved after the ` +
|
|
142
|
+
'page or node was already cleaned up; the teardown was dropped and that mount may ' +
|
|
143
|
+
'leak. Use react-on-rails-pro for reliable async-renderer teardown on fast navigations.');
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.catch((error) => {
|
|
147
|
+
const isStillActive = renderedRoots.get(domNodeId) === entry;
|
|
148
|
+
if (!isStillActive) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
renderedRoots.delete(domNodeId);
|
|
152
|
+
// The renderer's own promise rejected: the render failed, so the component never mounted and
|
|
153
|
+
// no teardown was captured. Log it (rather than letting it surface as an unhandled rejection)
|
|
154
|
+
// so the failure is diagnosable; any partial mount the renderer created may leak on cleanup.
|
|
155
|
+
// If this placeholder was already removed by page unload or node replacement, the page/node is
|
|
156
|
+
// already being cleaned up, so suppress a stale rejection log from the abandoned renderer.
|
|
157
|
+
console.error(`Renderer for dom node "${domNodeId}" rejected; the component did not mount and no ` +
|
|
158
|
+
'teardown was captured. Any mount it created may leak on cleanup:', error);
|
|
159
|
+
});
|
|
38
160
|
}
|
|
39
|
-
return false;
|
|
40
161
|
}
|
|
41
162
|
/**
|
|
42
163
|
* Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or
|
|
@@ -66,28 +187,28 @@ function renderElement(el, railsContext) {
|
|
|
66
187
|
}
|
|
67
188
|
return;
|
|
68
189
|
}
|
|
69
|
-
// DOM node was replaced (e.g., via async HTML injection) - clean up the old root
|
|
190
|
+
// DOM node was replaced (e.g., via async HTML injection) - clean up the old root or run
|
|
191
|
+
// the old renderer's teardown.
|
|
70
192
|
try {
|
|
71
|
-
|
|
72
|
-
existing.root &&
|
|
73
|
-
typeof existing.root === 'object' &&
|
|
74
|
-
'unmount' in existing.root) {
|
|
75
|
-
existing.root.unmount();
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
unmountComponentAtNode(existing.domNode);
|
|
79
|
-
}
|
|
193
|
+
teardownEntry(existing, domNodeId);
|
|
80
194
|
}
|
|
81
195
|
catch (unmountError) {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
196
|
+
// Surface the failure unconditionally (matching unmountAllComponents) so a teardown/unmount
|
|
197
|
+
// error on node replacement is as visible as one on page unload, using the same greppable
|
|
198
|
+
// labels. We still continue: the old mount may leak, but the new node must be rendered.
|
|
199
|
+
const label = existing.kind === 'renderer'
|
|
200
|
+
? `Error in renderer teardown for dom node "${domNodeId}":`
|
|
201
|
+
: `Error unmounting component for dom node "${domNodeId}":`;
|
|
202
|
+
console.error(label, unmountError);
|
|
86
203
|
}
|
|
87
204
|
renderedRoots.delete(domNodeId);
|
|
88
205
|
}
|
|
89
206
|
const componentObj = ComponentRegistry.get(name);
|
|
90
|
-
|
|
207
|
+
const delegation = delegateToRenderer(componentObj, props, railsContext, domNodeId, trace);
|
|
208
|
+
if (delegation.delegated) {
|
|
209
|
+
// The renderer owns its own mount; record it (with any teardown wrapper it returned) so it
|
|
210
|
+
// gets cleaned up on page unload or same-id node replacement.
|
|
211
|
+
trackRendererMount(domNodeId, domNode, delegation.result);
|
|
91
212
|
return;
|
|
92
213
|
}
|
|
93
214
|
// Hydrate if the DOM node has content (server-rendered HTML)
|
|
@@ -110,7 +231,7 @@ You should return a React.Component always for the client side entry point.`);
|
|
|
110
231
|
else {
|
|
111
232
|
const root = reactHydrateOrRender(domNode, reactElementOrRouterResult, shouldHydrate);
|
|
112
233
|
// Track the root for cleanup
|
|
113
|
-
renderedRoots.set(domNodeId, { root, domNode });
|
|
234
|
+
renderedRoots.set(domNodeId, { kind: 'react', root, domNode });
|
|
114
235
|
}
|
|
115
236
|
}
|
|
116
237
|
}
|
|
@@ -165,23 +286,22 @@ export function reactOnRailsComponentLoaded(domId) {
|
|
|
165
286
|
return Promise.resolve();
|
|
166
287
|
}
|
|
167
288
|
/**
|
|
168
|
-
* Unmount all rendered React components and clear roots.
|
|
169
|
-
*
|
|
289
|
+
* Unmount all rendered React components, run all renderer-function teardowns, and clear roots.
|
|
290
|
+
* Registered with `onPageUnloaded` to run on the page-unload lifecycle (Turbo/Turbolinks
|
|
291
|
+
* soft-navigation page swap, not a native browser unload) to prevent memory leaks.
|
|
170
292
|
*/
|
|
171
293
|
function unmountAllComponents() {
|
|
172
|
-
renderedRoots.forEach((
|
|
294
|
+
renderedRoots.forEach((entry, domNodeId) => {
|
|
173
295
|
try {
|
|
174
|
-
|
|
175
|
-
// React 18+ Root API
|
|
176
|
-
root.unmount();
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
// React 16-17 legacy API
|
|
180
|
-
unmountComponentAtNode(domNode);
|
|
181
|
-
}
|
|
296
|
+
teardownEntry(entry, domNodeId);
|
|
182
297
|
}
|
|
183
298
|
catch (error) {
|
|
184
|
-
|
|
299
|
+
// Use the same label as the async-rejection path so renderer-teardown failures are greppable
|
|
300
|
+
// whether the teardown threw synchronously (here) or rejected (invokeRendererTeardown).
|
|
301
|
+
const label = entry.kind === 'renderer'
|
|
302
|
+
? `Error in renderer teardown for dom node "${domNodeId}":`
|
|
303
|
+
: `Error unmounting component for dom node "${domNodeId}":`;
|
|
304
|
+
console.error(label, error);
|
|
185
305
|
}
|
|
186
306
|
});
|
|
187
307
|
renderedRoots.clear();
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import type { RegisteredComponent,
|
|
1
|
+
import type { RegisteredComponent, RegisteredComponentValue } from './types/index.ts';
|
|
2
|
+
type RegisteredComponentEntry = RegisteredComponent<RegisteredComponentValue>;
|
|
2
3
|
declare const _default: {
|
|
3
4
|
/**
|
|
4
5
|
* @param components { component1: component1, component2: component2, etc. }
|
|
5
6
|
*/
|
|
6
|
-
register(components: Record<string,
|
|
7
|
+
register(components: Record<string, RegisteredComponentValue>): void;
|
|
7
8
|
/**
|
|
8
9
|
* @param name
|
|
9
10
|
* @returns { name, component, renderFunction, isRenderer }
|
|
10
11
|
*/
|
|
11
|
-
get(name: string):
|
|
12
|
+
get(name: string): RegisteredComponentEntry;
|
|
12
13
|
/**
|
|
13
14
|
* Get a Map containing all registered components. Useful for debugging.
|
|
14
15
|
* @returns Map where key is the component name and values are the
|
|
15
16
|
* { name, component, renderFunction, isRenderer}
|
|
16
17
|
*/
|
|
17
|
-
components(): Map<string,
|
|
18
|
+
components(): Map<string, RegisteredComponentEntry>;
|
|
18
19
|
/**
|
|
19
20
|
* Pro-only method that waits for component registration
|
|
20
21
|
* @param _name Component name to wait for
|
package/lib/base/client.d.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* @deprecated Use `capabilities/core.ts` instead. This file is kept for backward compatibility
|
|
3
3
|
* with older versions of react-on-rails-pro that import from `react-on-rails/@internal/base/client`.
|
|
4
4
|
*/
|
|
5
|
-
import type { RegisteredComponent,
|
|
5
|
+
import type { RegisteredComponent, RegisteredComponentValue, Store, StoreGenerator, ReactOnRailsInternal } from '../types/index.ts';
|
|
6
|
+
type RegisteredComponentEntry = RegisteredComponent<RegisteredComponentValue>;
|
|
6
7
|
interface Registries {
|
|
7
8
|
ComponentRegistry: {
|
|
8
|
-
register: (components: Record<string,
|
|
9
|
-
get: (name: string) =>
|
|
10
|
-
components: () => Map<string,
|
|
9
|
+
register: (components: Record<string, RegisteredComponentValue>) => void;
|
|
10
|
+
get: (name: string) => RegisteredComponentEntry;
|
|
11
|
+
components: () => Map<string, RegisteredComponentEntry>;
|
|
11
12
|
};
|
|
12
13
|
StoreRegistry: {
|
|
13
14
|
register: (storeGenerators: Record<string, StoreGenerator>) => void;
|
package/lib/base/client.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as Authenticity from "../Authenticity.js";
|
|
|
6
6
|
import buildConsoleReplay, { consoleReplay } from "../buildConsoleReplay.js";
|
|
7
7
|
import reactHydrateOrRender from "../reactHydrateOrRender.js";
|
|
8
8
|
import createReactOutput from "../createReactOutput.js";
|
|
9
|
+
import componentRegistrationMetric from "../componentRegistrationMetric.js";
|
|
9
10
|
const DEFAULT_OPTIONS = {
|
|
10
11
|
traceTurbolinks: false,
|
|
11
12
|
turbo: false,
|
|
@@ -134,8 +135,8 @@ Fix: Use only react-on-rails OR react-on-rails-pro, not both.`);
|
|
|
134
135
|
if (this.options.debugMode) {
|
|
135
136
|
componentNames.forEach((name) => {
|
|
136
137
|
const component = components[name];
|
|
137
|
-
const
|
|
138
|
-
console.log(`[ReactOnRails] ✅ Registered: ${name} (${
|
|
138
|
+
const registrationMetric = componentRegistrationMetric(component);
|
|
139
|
+
console.log(`[ReactOnRails] ✅ Registered: ${name} (${registrationMetric.value} ${registrationMetric.label})`);
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
142
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
|
-
import type { RegisteredComponent, RenderReturnType,
|
|
2
|
+
import type { RegisteredComponent, RenderReturnType, RegisteredComponentValue, AuthenticityHeaders, Store, StoreGenerator, ReactOnRailsOptions } from '../types/index.ts';
|
|
3
|
+
type RegisteredComponentEntry = RegisteredComponent<RegisteredComponentValue>;
|
|
3
4
|
export interface Registries {
|
|
4
5
|
ComponentRegistry: {
|
|
5
|
-
register: (components: Record<string,
|
|
6
|
-
get: (name: string) =>
|
|
7
|
-
components: () => Map<string,
|
|
6
|
+
register: (components: Record<string, RegisteredComponentValue>) => void;
|
|
7
|
+
get: (name: string) => RegisteredComponentEntry;
|
|
8
|
+
components: () => Map<string, RegisteredComponentEntry>;
|
|
8
9
|
};
|
|
9
10
|
StoreRegistry: {
|
|
10
11
|
register: (storeGenerators: Record<string, StoreGenerator>) => void;
|
|
@@ -31,22 +32,22 @@ export declare function createCoreCapability(registries: Registries): {
|
|
|
31
32
|
buildConsoleReplay(): string;
|
|
32
33
|
getConsoleReplayScript(): string;
|
|
33
34
|
resetOptions(): void;
|
|
34
|
-
register(components: Record<string,
|
|
35
|
+
register(components: Record<string, RegisteredComponentValue>): void;
|
|
35
36
|
registerStore(stores: Record<string, StoreGenerator>): void;
|
|
36
37
|
registerStoreGenerators(storeGenerators: Record<string, StoreGenerator>): void;
|
|
37
38
|
getStore(name: string, throwIfMissing?: boolean): Store | undefined;
|
|
38
39
|
getStoreGenerator(name: string): StoreGenerator;
|
|
39
40
|
setStore(name: string, store: Store): void;
|
|
40
41
|
clearHydratedStores(): void;
|
|
41
|
-
getComponent(name: string):
|
|
42
|
-
registeredComponents(): Map<string,
|
|
42
|
+
getComponent(name: string): RegisteredComponentEntry;
|
|
43
|
+
registeredComponents(): Map<string, RegisteredComponentEntry>;
|
|
43
44
|
storeGenerators(): Map<string, StoreGenerator>;
|
|
44
45
|
stores(): Map<string, Store>;
|
|
45
|
-
render(name: string, props: Record<string,
|
|
46
|
+
render(name: string, props: Record<string, unknown>, domNodeId: string, hydrate: boolean): RenderReturnType;
|
|
46
47
|
serverRenderReactComponent(...args: any[]): any;
|
|
47
48
|
handleError(...args: any[]): any;
|
|
48
49
|
prepareRenderResult(...args: any[]): any;
|
|
49
|
-
getOrWaitForComponent(): Promise<
|
|
50
|
+
getOrWaitForComponent(): Promise<RegisteredComponentEntry>;
|
|
50
51
|
getOrWaitForStore(): Promise<Store>;
|
|
51
52
|
getOrWaitForStoreGenerator(): Promise<StoreGenerator>;
|
|
52
53
|
reactOnRailsStoreLoaded(): Promise<void>;
|
|
@@ -55,4 +56,5 @@ export declare function createCoreCapability(registries: Registries): {
|
|
|
55
56
|
addAsyncPropsCapabilityToComponentProps(...args: any[]): any;
|
|
56
57
|
getOrCreateAsyncPropsManager(...args: any[]): any;
|
|
57
58
|
};
|
|
59
|
+
export {};
|
|
58
60
|
//# sourceMappingURL=core.d.ts.map
|
package/lib/capabilities/core.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as Authenticity from "../Authenticity.js";
|
|
|
2
2
|
import buildConsoleReplay, { consoleReplay } from "../buildConsoleReplay.js";
|
|
3
3
|
import reactHydrateOrRender from "../reactHydrateOrRender.js";
|
|
4
4
|
import createReactOutput from "../createReactOutput.js";
|
|
5
|
+
import componentRegistrationMetric from "../componentRegistrationMetric.js";
|
|
5
6
|
const DEFAULT_OPTIONS = {
|
|
6
7
|
traceTurbolinks: false,
|
|
7
8
|
turbo: false,
|
|
@@ -82,8 +83,8 @@ export function createCoreCapability(registries) {
|
|
|
82
83
|
if (this.options.debugMode) {
|
|
83
84
|
componentNames.forEach((name) => {
|
|
84
85
|
const component = components[name];
|
|
85
|
-
const
|
|
86
|
-
console.log(`[ReactOnRails] ✅ Registered: ${name} (${
|
|
86
|
+
const registrationMetric = componentRegistrationMetric(component);
|
|
87
|
+
console.log(`[ReactOnRails] ✅ Registered: ${name} (${registrationMetric.value} ${registrationMetric.label})`);
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RegisteredComponentValue } from './types/index.ts';
|
|
2
|
+
type RegistrationMetric = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
};
|
|
6
|
+
export default function componentRegistrationMetric(component: RegisteredComponentValue): RegistrationMetric;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=componentRegistrationMetric.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default function componentRegistrationMetric(component) {
|
|
2
|
+
if (typeof component === 'function') {
|
|
3
|
+
return { label: 'source chars', value: component.toString().length };
|
|
4
|
+
}
|
|
5
|
+
return { label: 'export keys', value: Object.keys(component).length };
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=componentRegistrationMetric.js.map
|
package/lib/createReactOutput.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { createElement, isValidElement } from 'react';
|
|
2
2
|
import { isServerRenderHash, isPromise } from "./isServerRenderResult.js";
|
|
3
|
+
import { isRendererTeardownResult } from "./rendererTeardown.js";
|
|
4
|
+
const unsupportedManualRendererMessage = (name) => `ReactOnRails.render() does not support renderer functions ("${name}"). ` +
|
|
5
|
+
'Use normal React on Rails component rendering so renderer teardowns are captured on navigation.';
|
|
6
|
+
function isReactObjectComponentType(value) {
|
|
7
|
+
if (value == null || typeof value !== 'object') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
// React.memo, React.forwardRef, React.lazy, and related component types are non-callable
|
|
11
|
+
// objects tagged with React's element-type marker.
|
|
12
|
+
const typeMarker = value.$$typeof;
|
|
13
|
+
return typeof typeMarker === 'symbol' || typeof typeMarker === 'number';
|
|
14
|
+
}
|
|
15
|
+
function isReactComponentType(value) {
|
|
16
|
+
return typeof value === 'function' || typeof value === 'string' || isReactObjectComponentType(value);
|
|
17
|
+
}
|
|
3
18
|
function createReactElementFromRenderFunctionResult(renderFunctionResult, name, props) {
|
|
4
19
|
if (isValidElement(renderFunctionResult)) {
|
|
5
20
|
// If already a ReactElement, then just return it.
|
|
@@ -24,7 +39,7 @@ work if you return JSX. Update by wrapping the result JSX of ${name} in a fat ar
|
|
|
24
39
|
* @returns {ReactElement}
|
|
25
40
|
*/
|
|
26
41
|
export default function createReactOutput({ componentObj, props, railsContext, domNodeId, trace, shouldHydrate, }) {
|
|
27
|
-
const { name, component, renderFunction } = componentObj;
|
|
42
|
+
const { name, component, renderFunction, isRenderer } = componentObj;
|
|
28
43
|
if (trace) {
|
|
29
44
|
if (railsContext && railsContext.serverSide) {
|
|
30
45
|
console.log(`RENDERED ${name} to dom node with id: ${domNodeId}`);
|
|
@@ -41,7 +56,26 @@ export default function createReactOutput({ componentObj, props, railsContext, d
|
|
|
41
56
|
if (trace) {
|
|
42
57
|
console.log(`${name} is a renderFunction`);
|
|
43
58
|
}
|
|
59
|
+
// createReactOutput only handles render-functions that return a component or server-render hash.
|
|
60
|
+
// The 3-argument renderer form (which owns its own mount and may return a RendererTeardownResult)
|
|
61
|
+
// never reaches here on the supported paths: the client renderers delegate it earlier
|
|
62
|
+
// (`delegateToRenderer`) and return before this call, and server rendering rejects it upstream in
|
|
63
|
+
// validateComponent ("Detected a renderer while server rendering"). The only path that reaches
|
|
64
|
+
// here with a renderer is the manual public `ReactOnRails.render()` API, where renderers are
|
|
65
|
+
// unsupported because their teardown can't be tracked for cleanup. Reject it loudly and *before*
|
|
66
|
+
// invoking, rather than calling it with no domNodeId and rendering a half-wired, leak-prone result.
|
|
67
|
+
if (isRenderer) {
|
|
68
|
+
throw new Error(unsupportedManualRendererMessage(name));
|
|
69
|
+
}
|
|
70
|
+
if (typeof component !== 'function') {
|
|
71
|
+
throw new Error(`Registered render function "${name}" must be a function.`);
|
|
72
|
+
}
|
|
44
73
|
const renderFunctionResult = component(props, railsContext);
|
|
74
|
+
// Defense-in-depth: a 2-argument render function isn't expected to return a teardown wrapper, but
|
|
75
|
+
// the public RenderFunction return type can't structurally exclude it, so reject that at runtime too.
|
|
76
|
+
if (isRendererTeardownResult(renderFunctionResult)) {
|
|
77
|
+
throw new Error(unsupportedManualRendererMessage(name));
|
|
78
|
+
}
|
|
45
79
|
if (isServerRenderHash(renderFunctionResult)) {
|
|
46
80
|
// We just return at this point, because calling function knows how to handle this case and
|
|
47
81
|
// we can't call React.createElement with this type of Object.
|
|
@@ -51,6 +85,9 @@ export default function createReactOutput({ componentObj, props, railsContext, d
|
|
|
51
85
|
// We just return at this point, because calling function knows how to handle this case and
|
|
52
86
|
// we can't call React.createElement with this type of Object.
|
|
53
87
|
return renderFunctionResult.then((result) => {
|
|
88
|
+
if (isRendererTeardownResult(result)) {
|
|
89
|
+
throw new Error(unsupportedManualRendererMessage(name));
|
|
90
|
+
}
|
|
54
91
|
// If the result is a function, then it returned a React Component (even class components are functions).
|
|
55
92
|
if (typeof result === 'function') {
|
|
56
93
|
return createReactElementFromRenderFunctionResult(result, name, props);
|
|
@@ -60,7 +97,9 @@ export default function createReactOutput({ componentObj, props, railsContext, d
|
|
|
60
97
|
}
|
|
61
98
|
return createReactElementFromRenderFunctionResult(renderFunctionResult, name, props);
|
|
62
99
|
}
|
|
63
|
-
|
|
100
|
+
if (!isReactComponentType(component)) {
|
|
101
|
+
throw new Error(`Registered component "${name}" must be a function, string, or React object component type.`);
|
|
102
|
+
}
|
|
64
103
|
return createElement(component, props);
|
|
65
104
|
}
|
|
66
105
|
//# sourceMappingURL=createReactOutput.js.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RegisteredComponentValue, RenderFunction, RendererFunction } from './types/index.ts';
|
|
2
|
+
type AnyRenderFunction = RenderFunction | RendererFunction;
|
|
2
3
|
/**
|
|
3
|
-
* Used to determine we'll call
|
|
4
|
-
* Render-Function used return a function that takes props to return a React element
|
|
4
|
+
* Used to determine whether we'll call React.createElement on the component or if this is a
|
|
5
|
+
* Render-Function used to return a function that takes props to return a React element
|
|
5
6
|
* @param component
|
|
6
7
|
* @returns {boolean}
|
|
7
8
|
*/
|
|
8
|
-
export default function isRenderFunction(component:
|
|
9
|
+
export default function isRenderFunction(component: RegisteredComponentValue): component is AnyRenderFunction;
|
|
10
|
+
export {};
|
|
9
11
|
//# sourceMappingURL=isRenderFunction.d.ts.map
|
package/lib/isRenderFunction.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Used to determine we'll call
|
|
3
|
-
* Render-Function used return a function that takes props to return a React element
|
|
2
|
+
* Used to determine whether we'll call React.createElement on the component or if this is a
|
|
3
|
+
* Render-Function used to return a function that takes props to return a React element
|
|
4
4
|
* @param component
|
|
5
5
|
* @returns {boolean}
|
|
6
6
|
*/
|
|
7
7
|
export default function isRenderFunction(component) {
|
|
8
|
+
if (typeof component !== 'function') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const callableComponent = component;
|
|
8
12
|
// No for es5 or es6 React Component
|
|
9
13
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
10
|
-
if (
|
|
14
|
+
if (callableComponent.prototype?.isReactComponent) {
|
|
11
15
|
return false;
|
|
12
16
|
}
|
|
13
|
-
if (
|
|
17
|
+
if (callableComponent.renderFunction) {
|
|
14
18
|
return true;
|
|
15
19
|
}
|
|
16
20
|
// If zero or one args, then we know that this is a regular function that will
|
|
17
21
|
// return a React component
|
|
18
|
-
if (
|
|
22
|
+
if (callableComponent.length >= 2) {
|
|
19
23
|
return true;
|
|
20
24
|
}
|
|
21
25
|
return false;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Shared type guard for RendererTeardownResult, extracted so OSS and Pro can import it without
|
|
2
|
+
// duplicating the predicate or coupling Pro to ClientRenderer internals.
|
|
3
|
+
// eslint-disable-next-line import/prefer-default-export -- single-export module; named export keeps the type guard's API tied to its predicate name.
|
|
4
|
+
export function isRendererTeardownResult(value) {
|
|
5
|
+
return (value != null &&
|
|
6
|
+
typeof value === 'object' &&
|
|
7
|
+
typeof value.teardown === 'function');
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=rendererTeardown.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RegisteredComponent, RenderingError, FinalHtmlResult } from './types/index.ts';
|
|
1
|
+
import type { RegisteredComponent, RegisteredComponentValue, RenderingError, FinalHtmlResult } from './types/index.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Builds the metadata object for the length-prefixed streaming protocol.
|
|
4
4
|
* This is the shared metadata builder used by both streaming and non-streaming paths.
|
|
@@ -20,6 +20,6 @@ export declare function buildRenderMetadata(consoleReplayScript: string, renderS
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function buildLengthPrefixedResult(html: FinalHtmlResult | null, consoleReplayScript: string, renderState: RenderMetadataSource): string;
|
|
22
22
|
export declare function convertToError(e: unknown): Error;
|
|
23
|
-
export declare function validateComponent(componentObj: RegisteredComponent
|
|
23
|
+
export declare function validateComponent(componentObj: RegisteredComponent<RegisteredComponentValue>, componentName: string): void;
|
|
24
24
|
export {};
|
|
25
25
|
//# sourceMappingURL=serverRenderUtils.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactElement, ReactNode, Component, ComponentType } from 'react';
|
|
1
|
+
import type { ReactElement, ReactNode, Component, ComponentType, ExoticComponent } from 'react';
|
|
2
2
|
import type { PipeableStream } from 'react-dom/server';
|
|
3
3
|
import type { Readable } from 'stream';
|
|
4
4
|
/**
|
|
@@ -10,7 +10,7 @@ import type { Readable } from 'stream';
|
|
|
10
10
|
type Store = {
|
|
11
11
|
getState(): unknown;
|
|
12
12
|
};
|
|
13
|
-
type ReactComponent = ComponentType<any> | string;
|
|
13
|
+
type ReactComponent = ComponentType<any> | ExoticComponent<any> | string;
|
|
14
14
|
export type RailsContext = {
|
|
15
15
|
componentRegistryTimeout: number;
|
|
16
16
|
railsEnv: string;
|
|
@@ -76,6 +76,68 @@ type CreateReactOutputResult = CreateReactOutputSyncResult | CreateReactOutputAs
|
|
|
76
76
|
type RenderFunctionSyncResult = ReactComponent | ServerRenderResult;
|
|
77
77
|
type RenderFunctionAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactComponent | ServerRenderResult>;
|
|
78
78
|
type RenderFunctionResult = RenderFunctionSyncResult | RenderFunctionAsyncResult;
|
|
79
|
+
type ReactComponentRenderFunctionResult = ReactComponent | Promise<ReactComponent>;
|
|
80
|
+
/**
|
|
81
|
+
* Optional cleanup callback that a renderer function (the 3-argument form
|
|
82
|
+
* `(props, railsContext, domNodeId) => …`) may return inside a {@link RendererTeardownResult}.
|
|
83
|
+
* React on Rails invokes it when the mount is torn down — on Turbo/Turbolinks navigation (the
|
|
84
|
+
* framework's soft-navigation page swap, not a native browser unload) and when the same `domNodeId`
|
|
85
|
+
* node is replaced — so renderer-managed React roots, event listeners, and subscriptions are released
|
|
86
|
+
* instead of leaked. May be synchronous or asynchronous.
|
|
87
|
+
*
|
|
88
|
+
* @see RenderFunction
|
|
89
|
+
*/
|
|
90
|
+
type RendererTeardownReturn = void | Promise<void>;
|
|
91
|
+
type RendererTeardown = () => RendererTeardownReturn;
|
|
92
|
+
/**
|
|
93
|
+
* Object wrapper returned by a 3-argument renderer to opt into cleanup. The wrapper keeps teardown
|
|
94
|
+
* detection unambiguous: legacy renderers may have returned function components before this contract
|
|
95
|
+
* existed, so a bare function return is treated as no teardown.
|
|
96
|
+
*/
|
|
97
|
+
type RendererTeardownResult = {
|
|
98
|
+
teardown: RendererTeardown;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* What the 3-argument renderer form may return for cleanup: nothing, or a
|
|
102
|
+
* {@link RendererTeardownResult}. Runtime cleanup only recognizes this explicit wrapper; legacy
|
|
103
|
+
* component/server-result returns from 3-argument renderers are ignored.
|
|
104
|
+
*
|
|
105
|
+
* Consumers discriminate this union at runtime by an object with a `teardown` function vs. a thenable
|
|
106
|
+
* (an async renderer, awaited/adopted before re-checking) vs. anything else (no teardown). The
|
|
107
|
+
* `void` arm is therefore treated as "no teardown," same as `undefined`.
|
|
108
|
+
*/
|
|
109
|
+
type RendererResult = void | RendererTeardownResult | Promise<void | RendererTeardownResult>;
|
|
110
|
+
type RendererFunctionResult = RendererResult | RenderFunctionResult;
|
|
111
|
+
interface RenderFunctionMarker {
|
|
112
|
+
renderFunction?: true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The precise call signature of the 3-argument "renderer" form `(props, railsContext, domNodeId) =>
|
|
116
|
+
* …`. A renderer owns its own mount and may return nothing or a {@link RendererTeardownResult}
|
|
117
|
+
* (possibly async) to opt into cleanup. It also accepts the legacy {@link RenderFunctionResult}
|
|
118
|
+
* return shapes because older 3-argument renderers sometimes returned a component only to satisfy
|
|
119
|
+
* the old `RenderFunction` type; those non-teardown values are ignored at runtime. Shared by the
|
|
120
|
+
* core and Pro client renderers so the two cannot drift.
|
|
121
|
+
*
|
|
122
|
+
* @returns New renderer code should return `void` or `{ teardown }`. The broader legacy return
|
|
123
|
+
* shapes stay accepted only so existing 3-argument renderers remain type-compatible.
|
|
124
|
+
*/
|
|
125
|
+
interface RendererFunction extends RenderFunctionMarker {
|
|
126
|
+
(props?: Record<string, unknown>, railsContext?: RailsContext, domNodeId?: string): RendererFunctionResult;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A render function variant for APIs that require a React component result, such as
|
|
130
|
+
* Pro server-component wrappers. Unlike {@link RenderFunction}, this does not allow
|
|
131
|
+
* server-render hashes/HTML, and unlike {@link RendererFunction}, this does not allow
|
|
132
|
+
* renderer teardown results.
|
|
133
|
+
*
|
|
134
|
+
* Runtime render-function detection still follows the regular React on Rails
|
|
135
|
+
* convention: declare at least two parameters, or set `renderFunction = true`
|
|
136
|
+
* on one-argument functions.
|
|
137
|
+
*/
|
|
138
|
+
interface ReactComponentRenderFunction<Props = any> extends RenderFunctionMarker {
|
|
139
|
+
(props?: Props, railsContext?: RailsContext, domNodeId?: string): ReactComponentRenderFunctionResult;
|
|
140
|
+
}
|
|
79
141
|
type StreamableComponentResult = ReactElement | Promise<ReactElement | string>;
|
|
80
142
|
type AsyncPropsManager = {
|
|
81
143
|
getProp: (propName: string) => Promise<unknown>;
|
|
@@ -105,17 +167,34 @@ type AsyncPropsManager = {
|
|
|
105
167
|
* // Option 2: Using renderFunction property
|
|
106
168
|
* const anotherRenderFunction = (props) => { ... };
|
|
107
169
|
* anotherRenderFunction.renderFunction = true;
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* The 3-argument "renderer" form `(props, railsContext, domNodeId)` owns its own DOM
|
|
173
|
+
* rendering/hydration. Use {@link RendererFunction} for renderers that return nothing or an optional
|
|
174
|
+
* `{ teardown }` wrapper for cleanup. `RenderFunction` still accepts legacy 3-argument renderers
|
|
175
|
+
* that returned a component/server result only to satisfy the old type; React on Rails ignores those
|
|
176
|
+
* return values on the client renderer path.
|
|
108
177
|
*/
|
|
109
|
-
interface
|
|
178
|
+
interface ServerRenderFunction extends RenderFunctionMarker {
|
|
179
|
+
(props?: any, railsContext?: RailsContext): RenderFunctionResult;
|
|
180
|
+
}
|
|
181
|
+
interface LegacyRendererRenderFunction extends RenderFunctionMarker {
|
|
110
182
|
(props?: any, railsContext?: RailsContext, domNodeId?: string): RenderFunctionResult;
|
|
111
|
-
renderFunction?: true;
|
|
112
183
|
}
|
|
113
|
-
type
|
|
184
|
+
type RenderFunction = ServerRenderFunction | LegacyRendererRenderFunction;
|
|
185
|
+
type ReactComponentOrRenderFunction = ReactComponent | RenderFunction | RendererFunction;
|
|
186
|
+
type RegisteredComponentValue = ReactComponentOrRenderFunction | Record<string, unknown>;
|
|
114
187
|
type PipeableOrReadableStream = PipeableStream | NodeJS.ReadableStream;
|
|
115
|
-
export type { ReactComponentOrRenderFunction, ReactComponent, AuthenticityHeaders, RenderFunction, RenderFunctionResult, Store, StoreGenerator, CreateReactOutputResult, ServerRenderResult, ServerRenderHashRenderedHtml, CreateReactOutputSyncResult, CreateReactOutputAsyncResult, RenderFunctionSyncResult, RenderFunctionAsyncResult, StreamableComponentResult, PipeableOrReadableStream, };
|
|
116
|
-
|
|
188
|
+
export type { ReactComponentOrRenderFunction, RegisteredComponentValue, ReactComponent, ReactComponentRenderFunction, AuthenticityHeaders, RenderFunction, RendererTeardown, RendererTeardownResult, RendererFunction, RenderFunctionResult, Store, StoreGenerator, CreateReactOutputResult, ServerRenderResult, ServerRenderHashRenderedHtml, CreateReactOutputSyncResult, CreateReactOutputAsyncResult, RenderFunctionSyncResult, RenderFunctionAsyncResult, ReactComponentRenderFunctionResult, StreamableComponentResult, PipeableOrReadableStream, };
|
|
189
|
+
/**
|
|
190
|
+
* The generic defaults to the pre-object-registration component type so existing consumers that
|
|
191
|
+
* read `registeredComponent.component` stay source-compatible. Use
|
|
192
|
+
* `RegisteredComponent<RegisteredComponentValue>` when handling plain-object server_render_js
|
|
193
|
+
* registrations.
|
|
194
|
+
*/
|
|
195
|
+
export interface RegisteredComponent<ComponentValue extends RegisteredComponentValue = ReactComponentOrRenderFunction> {
|
|
117
196
|
name: string;
|
|
118
|
-
component:
|
|
197
|
+
component: ComponentValue;
|
|
119
198
|
/**
|
|
120
199
|
* Indicates if the registered component is a RenderFunction
|
|
121
200
|
* @see RenderFunction for more details on its behavior and usage.
|
|
@@ -141,7 +220,7 @@ export interface RSCRenderParams extends Omit<RenderParams, 'railsContext'> {
|
|
|
141
220
|
railsContext: RailsContextWithServerStreamingCapabilities;
|
|
142
221
|
}
|
|
143
222
|
export interface CreateParams extends Params {
|
|
144
|
-
componentObj: RegisteredComponent
|
|
223
|
+
componentObj: RegisteredComponent<RegisteredComponentValue>;
|
|
145
224
|
shouldHydrate?: boolean;
|
|
146
225
|
}
|
|
147
226
|
export interface ErrorOptions {
|
|
@@ -187,7 +266,7 @@ export interface ReactOnRails {
|
|
|
187
266
|
* find you components for rendering.
|
|
188
267
|
* @param components keys are component names, values are components
|
|
189
268
|
*/
|
|
190
|
-
register(components: Record<string,
|
|
269
|
+
register(components: Record<string, RegisteredComponentValue>): void;
|
|
191
270
|
/** @deprecated Use registerStoreGenerators instead */
|
|
192
271
|
registerStore(stores: Record<string, StoreGenerator>): void;
|
|
193
272
|
/**
|
|
@@ -300,6 +379,17 @@ export interface ReactOnRailsInternal extends ReactOnRails {
|
|
|
300
379
|
* ```
|
|
301
380
|
* under React 18+.
|
|
302
381
|
*
|
|
382
|
+
* @remarks
|
|
383
|
+
* **Cleanup is the caller's responsibility.** Unlike the components React on Rails mounts itself
|
|
384
|
+
* (which are unmounted automatically on Turbo/Turbolinks navigation and same-id node replacement),
|
|
385
|
+
* a root created by this imperative API is **not** tracked internally. The returned root is handed
|
|
386
|
+
* back to you, and you must call `unmount()` on it yourself — e.g. on a Turbo `turbo:before-render`
|
|
387
|
+
* / Turbolinks `turbolinks:before-render` event, or in your framework's teardown hook — to avoid
|
|
388
|
+
* leaking the root (and any subscriptions or timers it holds) across navigations. If you want
|
|
389
|
+
* automatic cleanup instead, register a renderer function (the 3-argument render-function form) and
|
|
390
|
+
* return a {@link RendererTeardownResult}; React on Rails tracks those mounts and runs the teardown for
|
|
391
|
+
* you.
|
|
392
|
+
*
|
|
303
393
|
* @param name Name of your registered component
|
|
304
394
|
* @param props Props to pass to your component
|
|
305
395
|
* @param domNodeId HTML ID of the node the component will be rendered at
|
|
@@ -308,17 +398,17 @@ export interface ReactOnRailsInternal extends ReactOnRails {
|
|
|
308
398
|
* (see "What is a root?" in https://github.com/reactwg/react-18/discussions/5).
|
|
309
399
|
* Under React 16/17: Reference to your component's backing instance or `null` for stateless components.
|
|
310
400
|
*/
|
|
311
|
-
render(name: string, props: Record<string,
|
|
401
|
+
render(name: string, props: Record<string, unknown>, domNodeId: string, hydrate?: boolean): RenderReturnType;
|
|
312
402
|
/**
|
|
313
403
|
* Get the component that you registered
|
|
314
404
|
* @returns {name, component, renderFunction, isRenderer}
|
|
315
405
|
*/
|
|
316
|
-
getComponent(name: string): RegisteredComponent
|
|
406
|
+
getComponent(name: string): RegisteredComponent<RegisteredComponentValue>;
|
|
317
407
|
/**
|
|
318
408
|
* Get the component that you registered, or wait for it to be registered
|
|
319
409
|
* @returns {name, component, renderFunction, isRenderer}
|
|
320
410
|
*/
|
|
321
|
-
getOrWaitForComponent(name: string): Promise<RegisteredComponent
|
|
411
|
+
getOrWaitForComponent(name: string): Promise<RegisteredComponent<RegisteredComponentValue>>;
|
|
322
412
|
/**
|
|
323
413
|
* Used by server rendering by Rails
|
|
324
414
|
*/
|
|
@@ -353,7 +443,7 @@ export interface ReactOnRailsInternal extends ReactOnRails {
|
|
|
353
443
|
/**
|
|
354
444
|
* Get a Map containing all registered components. Useful for debugging.
|
|
355
445
|
*/
|
|
356
|
-
registeredComponents(): Map<string, RegisteredComponent
|
|
446
|
+
registeredComponents(): Map<string, RegisteredComponent<RegisteredComponentValue>>;
|
|
357
447
|
/**
|
|
358
448
|
* Get a Map containing all registered store generators. Useful for debugging.
|
|
359
449
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-on-rails",
|
|
3
|
-
"version": "17.0.0-rc.
|
|
3
|
+
"version": "17.0.0-rc.2",
|
|
4
4
|
"description": "react-on-rails JavaScript for react_on_rails Ruby gem",
|
|
5
5
|
"main": "lib/ReactOnRails.full.js",
|
|
6
6
|
"type": "module",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"./ReactDOMServer": "./lib/ReactDOMServer.cjs",
|
|
44
44
|
"./serverRenderReactComponent": "./lib/serverRenderReactComponent.js",
|
|
45
45
|
"./@internal/sanitizeNonce": "./lib/sanitizeNonce.js",
|
|
46
|
+
"./@internal/rendererTeardown": "./lib/rendererTeardown.js",
|
|
46
47
|
"./@internal/base/client": "./lib/base/client.js",
|
|
47
48
|
"./@internal/base/full": {
|
|
48
49
|
"react-server": "./lib/base/full.rsc.js",
|