11/* 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 */
33
44import * as ReactDOM from 'react-dom' ;
55import type { ReactElement } from 'react' ;
@@ -14,13 +14,13 @@ import { debugTurbolinks } from './turbolinksUtils';
1414
1515const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store' ;
1616
17- function delegateToRenderer (
17+ async function delegateToRenderer (
1818 componentObj : RegisteredComponent ,
19- props : Record < string , string > ,
19+ props : Record < string , unknown > ,
2020 railsContext : RailsContext ,
2121 domNodeId : string ,
2222 trace : boolean ,
23- ) : boolean {
23+ ) : Promise < boolean > {
2424 const { name, component, isRenderer } = componentObj ;
2525
2626 if ( isRenderer ) {
@@ -32,7 +32,7 @@ function delegateToRenderer(
3232 ) ;
3333 }
3434
35- ( component as RenderFunction ) ( props , railsContext , domNodeId ) ;
35+ await ( component as RenderFunction ) ( props , railsContext , domNodeId ) ;
3636 return true ;
3737 }
3838
@@ -81,7 +81,7 @@ class ComponentRenderer {
8181 // This must match lib/react_on_rails/helper.rb
8282 const name = el . getAttribute ( 'data-component-name' ) || '' ;
8383 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 > ) : { } ;
8585 const trace = el . getAttribute ( 'data-trace' ) === 'true' ;
8686
8787 try {
@@ -92,7 +92,11 @@ class ComponentRenderer {
9292 return ;
9393 }
9494
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+ ) {
96100 return ;
97101 }
98102
@@ -163,8 +167,8 @@ You should return a React.Component always for the client side entry point.`);
163167 }
164168
165169 waitUntilRendered ( ) : Promise < void > {
166- if ( this . state === 'rendering' ) {
167- return this . renderPromise ! ;
170+ if ( this . state === 'rendering' && this . renderPromise ) {
171+ return this . renderPromise ;
168172 }
169173 return Promise . resolve ( ) ;
170174 }
@@ -183,15 +187,18 @@ class StoreRenderer {
183187 }
184188
185189 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+ : { } ;
187194 this . hydratePromise = this . hydrate ( context , railsContext , name , props ) ;
188195 }
189196
190197 private async hydrate (
191198 context : Context ,
192199 railsContext : RailsContext ,
193200 name : string ,
194- props : Record < string , string > ,
201+ props : Record < string , unknown > ,
195202 ) {
196203 const storeGenerator = await context . ReactOnRails . getOrWaitForStoreGenerator ( name ) ;
197204 if ( this . state === 'unmounted' ) {
@@ -204,8 +211,8 @@ class StoreRenderer {
204211 }
205212
206213 waitUntilHydrated ( ) : Promise < void > {
207- if ( this . state === 'hydrating' ) {
208- return this . hydratePromise ! ;
214+ if ( this . state === 'hydrating' && this . hydratePromise ) {
215+ return this . hydratePromise ;
209216 }
210217 return Promise . resolve ( ) ;
211218 }
@@ -217,26 +224,30 @@ class StoreRenderer {
217224
218225const renderedRoots = new Map < string , ComponentRenderer > ( ) ;
219226
220- export function renderOrHydrateComponent ( domIdOrElement : string | Element ) : ComponentRenderer | undefined {
227+ export function renderOrHydrateComponent ( domIdOrElement : string | Element ) {
221228 const domId = getDomId ( domIdOrElement ) ;
222- debugTurbolinks ( ` renderOrHydrateComponent ${ domId } ` ) ;
229+ debugTurbolinks ( ' renderOrHydrateComponent' , domId ) ;
223230 let root = renderedRoots . get ( domId ) ;
224231 if ( ! root ) {
225232 root = new ComponentRenderer ( domIdOrElement ) ;
226233 renderedRoots . set ( domId , root ) ;
227234 }
228- return root ;
235+ return root . waitUntilRendered ( ) ;
229236}
230237
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 ) ) ;
234244}
235245
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 ) ;
240251
241252function unmountAllComponents ( ) : void {
242253 renderedRoots . forEach ( ( root ) => root . unmount ( ) ) ;
@@ -267,15 +278,11 @@ export async function hydrateStore(storeNameOrElement: string | Element) {
267278 await storeRenderer . waitUntilHydrated ( ) ;
268279}
269280
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 ) ;
274283
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 ) ;
279286
280287function unmountAllStores ( ) : void {
281288 storeRenderers . forEach ( ( storeRenderer ) => storeRenderer . unmount ( ) ) ;
0 commit comments