@@ -253,6 +253,12 @@ export default function Playground({
253253 */
254254 const [ resetCount , setResetCount ] = useState ( 0 ) ;
255255
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+
256262 const setAndSaveMode = ( mode : Mode ) => {
257263 setIonicMode ( mode ) ;
258264
@@ -261,7 +267,7 @@ export default function Playground({
261267
262268 /**
263269 * 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 .
265271 */
266272 window . dispatchEvent (
267273 new CustomEvent ( MODE_UPDATED_EVENT , {
@@ -289,7 +295,7 @@ export default function Playground({
289295
290296 /**
291297 * 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 .
293299 */
294300 window . dispatchEvent (
295301 new CustomEvent ( USAGE_TARGET_UPDATED_EVENT , {
@@ -401,72 +407,92 @@ export default function Playground({
401407 } ) ;
402408
403409 /**
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.
409419 */
410420 useEffect ( ( ) => {
411421 const io = new IntersectionObserver (
412422 ( entries : IntersectionObserverEntry [ ] ) => {
413423 const ev = entries [ 0 ] ;
414- if ( ! ev . isIntersecting || renderIframes ) return ;
424+ setIsInView ( ev . isIntersecting ) ;
425+ if ( ! ev . isIntersecting ) return ;
415426
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+ }
417437
418438 /**
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.
421440 */
422- io . disconnect ( ) ;
441+ if ( ! renderIframes ) {
442+ setRenderIframes ( true ) ;
443+ }
423444 } ,
424445 { threshold : 0 }
425446 ) ;
426447
427448 io . observe ( hostRef . current ! ) ;
428449 } ) ;
429450
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+
430465 /**
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+ *
431472 * Sometimes, the app isn't fully hydrated on the first render,
432473 * causing isBrowser to be set to false even if running the app
433474 * 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.
435478 *
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.
440484 */
441485 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 ) ;
468489 }
469- } , [ isBrowser ] ) ;
490+
491+ return ( ) => {
492+ window . removeEventListener ( MODE_UPDATED_EVENT , handleModeUpdated ) ;
493+ window . removeEventListener ( USAGE_TARGET_UPDATED_EVENT , handleUsageTargetUpdated ) ;
494+ } ;
495+ } , [ isBrowser , isInView ] ) ;
470496
471497 const isIOS = ionicMode === Mode . iOS ;
472498 const isMD = ionicMode === Mode . MD ;
0 commit comments