@@ -253,6 +253,12 @@ export default function Playground({
253
253
*/
254
254
const [ resetCount , setResetCount ] = useState ( 0 ) ;
255
255
256
+ /**
257
+ * Keeps track of whether any amount of this playground is
258
+ * currently on the screen.
259
+ */
260
+ const [ isInView , setIsInView ] = useState ( false ) ;
261
+
256
262
const setAndSaveMode = ( mode : Mode ) => {
257
263
setIonicMode ( mode ) ;
258
264
@@ -261,7 +267,7 @@ export default function Playground({
261
267
262
268
/**
263
269
* Tell other playgrounds on the page that the mode has
264
- * updated, so they can sync up.
270
+ * updated, so they can sync up if they're in view .
265
271
*/
266
272
window . dispatchEvent (
267
273
new CustomEvent ( MODE_UPDATED_EVENT , {
@@ -289,7 +295,7 @@ export default function Playground({
289
295
290
296
/**
291
297
* Tell other playgrounds on the page that the framework
292
- * has updated, so they can sync up.
298
+ * has updated, so they can sync up if they're in view .
293
299
*/
294
300
window . dispatchEvent (
295
301
new CustomEvent ( USAGE_TARGET_UPDATED_EVENT , {
@@ -401,72 +407,92 @@ export default function Playground({
401
407
} ) ;
402
408
403
409
/**
404
- * By default, we do not render the iframe content
405
- * as it could cause delays on page load. Instead
406
- * we wait for even 1 pixel of the playground to
407
- * scroll into view (intersect with the viewport)
408
- * before loading the iframes.
410
+ * By default, we do not render the iframe content as it could
411
+ * cause delays on page load. We also do not immediately switch
412
+ * the framework/mode when it gets changed through another
413
+ * playground on the page, as switching them for every playground
414
+ * at once can cause memory-related crashes on some devices.
415
+ *
416
+ * Instead, we wait for even 1 pixel of the playground to scroll
417
+ * into view (intersect with the viewport) before loading the
418
+ * iframes or auto-switching the framework/mode.
409
419
*/
410
420
useEffect ( ( ) => {
411
421
const io = new IntersectionObserver (
412
422
( entries : IntersectionObserverEntry [ ] ) => {
413
423
const ev = entries [ 0 ] ;
414
- if ( ! ev . isIntersecting || renderIframes ) return ;
424
+ setIsInView ( ev . isIntersecting ) ;
425
+ if ( ! ev . isIntersecting ) return ;
415
426
416
- setRenderIframes ( true ) ;
427
+ /**
428
+ * Load the stored mode and/or usage target, if present
429
+ * from previously being toggled.
430
+ */
431
+ if ( isBrowser ) {
432
+ const storedMode = localStorage . getItem ( MODE_STORAGE_KEY ) ;
433
+ if ( storedMode ) setIonicMode ( storedMode ) ;
434
+ const storedUsageTarget = localStorage . getItem ( USAGE_TARGET_STORAGE_KEY ) ;
435
+ if ( storedUsageTarget ) setUsageTarget ( storedUsageTarget ) ;
436
+ }
417
437
418
438
/**
419
- * Once the playground is loaded, it is never "unloaded"
420
- * so we can safely disconnect the observer.
439
+ * If the iframes weren't already loaded, load them now.
421
440
*/
422
- io . disconnect ( ) ;
441
+ if ( ! renderIframes ) {
442
+ setRenderIframes ( true ) ;
443
+ }
423
444
} ,
424
445
{ threshold : 0 }
425
446
) ;
426
447
427
448
io . observe ( hostRef . current ! ) ;
428
449
} ) ;
429
450
451
+ const handleModeUpdated = ( e : CustomEvent ) => {
452
+ const mode = e . detail ;
453
+ if ( Object . values ( Mode ) . includes ( mode ) ) {
454
+ setIonicMode ( mode ) ; // don't use setAndSave to avoid infinite loop
455
+ }
456
+ } ;
457
+
458
+ const handleUsageTargetUpdated = ( e : CustomEvent ) => {
459
+ const usageTarget = e . detail ;
460
+ if ( Object . values ( UsageTarget ) . includes ( usageTarget ) ) {
461
+ setUsageTarget ( usageTarget ) ; // don't use setAndSave to avoid infinite loop
462
+ }
463
+ } ;
464
+
430
465
/**
466
+ * When this playground is in view, listen for any other playgrounds
467
+ * on the page to switch their framework or mode, so this one can
468
+ * sync up to the same setting. This is needed because if the
469
+ * playground is already in view, the IntersectionObserver doesn't
470
+ * fire until the playground is scrolled off and back on the screen.
471
+ *
431
472
* Sometimes, the app isn't fully hydrated on the first render,
432
473
* causing isBrowser to be set to false even if running the app
433
474
* in a browser (vs. SSR). isBrowser is then updated on the next
434
- * render cycle.
475
+ * render cycle. This means we need to re-run this hook when
476
+ * isBrowser changes to handle playgrounds that were in view
477
+ * from the start of the page load.
435
478
*
436
- * This useEffect contains code that can only run in the browser,
437
- * and also needs to run on that first go-around. Note that
438
- * isBrowser will never be set from true back to false, so the
439
- * code within the if(isBrowser) check will only run once.
479
+ * We also re-run when isInView changes because otherwise, a stale
480
+ * state value would be captured. Since we need to listen for these
481
+ * events only when the playground is in view, we check the state
482
+ * before adding the listeners at all, rather than within the
483
+ * callbacks.
440
484
*/
441
485
useEffect ( ( ) => {
442
- if ( isBrowser ) {
443
- /**
444
- * Load the stored mode and/or usage target, if present
445
- * from previously being toggled.
446
- */
447
- const storedMode = localStorage . getItem ( MODE_STORAGE_KEY ) ;
448
- if ( storedMode ) setIonicMode ( storedMode ) ;
449
- const storedUsageTarget = localStorage . getItem ( USAGE_TARGET_STORAGE_KEY ) ;
450
- if ( storedUsageTarget ) setUsageTarget ( storedUsageTarget ) ;
451
-
452
- /**
453
- * Listen for any playground on the page to have its mode or framework
454
- * updated so this playground can switch to the same setting.
455
- */
456
- window . addEventListener ( MODE_UPDATED_EVENT , ( e : CustomEvent ) => {
457
- const mode = e . detail ;
458
- if ( Object . values ( Mode ) . includes ( mode ) ) {
459
- setIonicMode ( mode ) ; // don't use setAndSave to avoid infinite loop
460
- }
461
- } ) ;
462
- window . addEventListener ( USAGE_TARGET_UPDATED_EVENT , ( e : CustomEvent ) => {
463
- const usageTarget = e . detail ;
464
- if ( Object . values ( UsageTarget ) . includes ( usageTarget ) ) {
465
- setUsageTarget ( usageTarget ) ; // don't use setAndSave to avoid infinite loop
466
- }
467
- } ) ;
486
+ if ( isBrowser && isInView ) {
487
+ window . addEventListener ( MODE_UPDATED_EVENT , handleModeUpdated ) ;
488
+ window . addEventListener ( USAGE_TARGET_UPDATED_EVENT , handleUsageTargetUpdated ) ;
468
489
}
469
- } , [ isBrowser ] ) ;
490
+
491
+ return ( ) => {
492
+ window . removeEventListener ( MODE_UPDATED_EVENT , handleModeUpdated ) ;
493
+ window . removeEventListener ( USAGE_TARGET_UPDATED_EVENT , handleUsageTargetUpdated ) ;
494
+ } ;
495
+ } , [ isBrowser , isInView ] ) ;
470
496
471
497
const isIOS = ionicMode === Mode . iOS ;
472
498
const isMD = ionicMode === Mode . MD ;
0 commit comments