Skip to content

Commit 3e4a34c

Browse files
fix(playground): wait until playground is in view before loading stored framework/mode (#3185)
1 parent 9914d65 commit 3e4a34c

File tree

1 file changed

+70
-44
lines changed

1 file changed

+70
-44
lines changed

src/components/global/Playground/index.tsx

+70-44
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)