1
1
/* eslint-disable max-classes-per-file */
2
- /* eslint-disable react/no-deprecated -- while we need to support React 16 */
2
+ /* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated -- while we need to support React 16 */
3
3
4
4
import * as ReactDOM from 'react-dom' ;
5
5
import type { ReactElement } from 'react' ;
@@ -14,13 +14,13 @@ import { debugTurbolinks } from './turbolinksUtils';
14
14
15
15
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store' ;
16
16
17
- function delegateToRenderer (
17
+ async function delegateToRenderer (
18
18
componentObj : RegisteredComponent ,
19
- props : Record < string , string > ,
19
+ props : Record < string , unknown > ,
20
20
railsContext : RailsContext ,
21
21
domNodeId : string ,
22
22
trace : boolean ,
23
- ) : boolean {
23
+ ) : Promise < boolean > {
24
24
const { name, component, isRenderer } = componentObj ;
25
25
26
26
if ( isRenderer ) {
@@ -32,7 +32,7 @@ function delegateToRenderer(
32
32
) ;
33
33
}
34
34
35
- ( component as RenderFunction ) ( props , railsContext , domNodeId ) ;
35
+ await ( component as RenderFunction ) ( props , railsContext , domNodeId ) ;
36
36
return true ;
37
37
}
38
38
@@ -81,7 +81,7 @@ class ComponentRenderer {
81
81
// This must match lib/react_on_rails/helper.rb
82
82
const name = el . getAttribute ( 'data-component-name' ) || '' ;
83
83
const { domNodeId } = this ;
84
- const props = el . textContent !== null ? JSON . parse ( el . textContent ) : { } ;
84
+ const props = el . textContent !== null ? ( JSON . parse ( el . textContent ) as Record < string , unknown > ) : { } ;
85
85
const trace = el . getAttribute ( 'data-trace' ) === 'true' ;
86
86
87
87
try {
@@ -92,7 +92,11 @@ class ComponentRenderer {
92
92
return ;
93
93
}
94
94
95
- if ( delegateToRenderer ( componentObj , props , railsContext , domNodeId , trace ) ) {
95
+ if (
96
+ ( await delegateToRenderer ( componentObj , props , railsContext , domNodeId , trace ) ) ||
97
+ // @ts -expect-error The state can change while awaiting delegateToRenderer
98
+ this . state === 'unmounted'
99
+ ) {
96
100
return ;
97
101
}
98
102
@@ -163,8 +167,8 @@ You should return a React.Component always for the client side entry point.`);
163
167
}
164
168
165
169
waitUntilRendered ( ) : Promise < void > {
166
- if ( this . state === 'rendering' ) {
167
- return this . renderPromise ! ;
170
+ if ( this . state === 'rendering' && this . renderPromise ) {
171
+ return this . renderPromise ;
168
172
}
169
173
return Promise . resolve ( ) ;
170
174
}
@@ -183,15 +187,18 @@ class StoreRenderer {
183
187
}
184
188
185
189
const name = storeDataElement . getAttribute ( REACT_ON_RAILS_STORE_ATTRIBUTE ) || '' ;
186
- const props = storeDataElement . textContent !== null ? JSON . parse ( storeDataElement . textContent ) : { } ;
190
+ const props =
191
+ storeDataElement . textContent !== null
192
+ ? ( JSON . parse ( storeDataElement . textContent ) as Record < string , unknown > )
193
+ : { } ;
187
194
this . hydratePromise = this . hydrate ( context , railsContext , name , props ) ;
188
195
}
189
196
190
197
private async hydrate (
191
198
context : Context ,
192
199
railsContext : RailsContext ,
193
200
name : string ,
194
- props : Record < string , string > ,
201
+ props : Record < string , unknown > ,
195
202
) {
196
203
const storeGenerator = await context . ReactOnRails . getOrWaitForStoreGenerator ( name ) ;
197
204
if ( this . state === 'unmounted' ) {
@@ -204,8 +211,8 @@ class StoreRenderer {
204
211
}
205
212
206
213
waitUntilHydrated ( ) : Promise < void > {
207
- if ( this . state === 'hydrating' ) {
208
- return this . hydratePromise ! ;
214
+ if ( this . state === 'hydrating' && this . hydratePromise ) {
215
+ return this . hydratePromise ;
209
216
}
210
217
return Promise . resolve ( ) ;
211
218
}
@@ -217,26 +224,30 @@ class StoreRenderer {
217
224
218
225
const renderedRoots = new Map < string , ComponentRenderer > ( ) ;
219
226
220
- export function renderOrHydrateComponent ( domIdOrElement : string | Element ) : ComponentRenderer | undefined {
227
+ export function renderOrHydrateComponent ( domIdOrElement : string | Element ) {
221
228
const domId = getDomId ( domIdOrElement ) ;
222
- debugTurbolinks ( ` renderOrHydrateComponent ${ domId } ` ) ;
229
+ debugTurbolinks ( ' renderOrHydrateComponent' , domId ) ;
223
230
let root = renderedRoots . get ( domId ) ;
224
231
if ( ! root ) {
225
232
root = new ComponentRenderer ( domIdOrElement ) ;
226
233
renderedRoots . set ( domId , root ) ;
227
234
}
228
- return root ;
235
+ return root . waitUntilRendered ( ) ;
229
236
}
230
237
231
- export function renderOrHydrateForceLoadedComponents ( ) : void {
232
- const els = document . querySelectorAll ( `.js-react-on-rails-component[data-force-load="true"]` ) ;
233
- els . forEach ( ( el ) => renderOrHydrateComponent ( el ) ) ;
238
+ async function forAllElementsAsync (
239
+ selector : string ,
240
+ callback : ( el : Element ) => Promise < void > ,
241
+ ) : Promise < void > {
242
+ const els = document . querySelectorAll ( selector ) ;
243
+ await Promise . all ( Array . from ( els ) . map ( callback ) ) ;
234
244
}
235
245
236
- export function renderOrHydrateAllComponents ( ) : void {
237
- const els = document . querySelectorAll ( `.js-react-on-rails-component` ) ;
238
- els . forEach ( ( el ) => renderOrHydrateComponent ( el ) ) ;
239
- }
246
+ export const renderOrHydrateForceLoadedComponents = ( ) =>
247
+ forAllElementsAsync ( '.js-react-on-rails-component[data-force-load="true"]' , renderOrHydrateComponent ) ;
248
+
249
+ export const renderOrHydrateAllComponents = ( ) =>
250
+ forAllElementsAsync ( '.js-react-on-rails-component' , renderOrHydrateComponent ) ;
240
251
241
252
function unmountAllComponents ( ) : void {
242
253
renderedRoots . forEach ( ( root ) => root . unmount ( ) ) ;
@@ -267,15 +278,11 @@ export async function hydrateStore(storeNameOrElement: string | Element) {
267
278
await storeRenderer . waitUntilHydrated ( ) ;
268
279
}
269
280
270
- export async function hydrateForceLoadedStores ( ) : Promise < void > {
271
- const els = document . querySelectorAll ( `[${ REACT_ON_RAILS_STORE_ATTRIBUTE } ][data-force-load="true"]` ) ;
272
- await Promise . all ( Array . from ( els ) . map ( ( el ) => hydrateStore ( el ) ) ) ;
273
- }
281
+ export const hydrateForceLoadedStores = ( ) =>
282
+ forAllElementsAsync ( `[${ REACT_ON_RAILS_STORE_ATTRIBUTE } ][data-force-load="true"]` , hydrateStore ) ;
274
283
275
- export async function hydrateAllStores ( ) : Promise < void > {
276
- const els = document . querySelectorAll ( `[${ REACT_ON_RAILS_STORE_ATTRIBUTE } ]` ) ;
277
- await Promise . all ( Array . from ( els ) . map ( ( el ) => hydrateStore ( el ) ) ) ;
278
- }
284
+ export const hydrateAllStores = ( ) =>
285
+ forAllElementsAsync ( `[${ REACT_ON_RAILS_STORE_ATTRIBUTE } ]` , hydrateStore ) ;
279
286
280
287
function unmountAllStores ( ) : void {
281
288
storeRenderers . forEach ( ( storeRenderer ) => storeRenderer . unmount ( ) ) ;
0 commit comments