fix(examples): direct pro-example URLs no longer hang on Loading example

The load effect listed the overlay's `settled` flag as a dependency. On
a direct /example/<pro-id> URL the load can begin the moment the
overlay registers the example — one microtask before the overlay's
import promise settles. The flip then re-fired the effect mid-load: the
cleanup cancelled it (setReady skipped), the re-run hit the loadedIdRef
guard and returned, and the page hung on "Loading example…" forever.

The effect now depends only on the example itself; the 404-vs-still-
loading decision moved into the render, where reading `settled` cancels
nothing. A load cancelled mid-flight also resets the loadedIdRef guard
so a genuine re-run reloads instead of early-returning.
This commit is contained in:
David Montero Crespo 2026-07-31 04:22:52 +02:00
parent 08c5626b79
commit 159417c927
1 changed files with 24 additions and 16 deletions

View File

@ -67,24 +67,21 @@ export const ExampleEditorPage: React.FC = () => {
); );
useEffect(() => { useEffect(() => {
if (!exampleId) { // `settled` is deliberately NOT a dependency. A direct link to a pro
setError(true); // example can begin loading the moment the overlay registers it — one
return; // microtask BEFORE the overlay's import promise settles. With `settled`
} // in the deps, that flip re-fired the effect mid-load: the cleanup set
if (!example) { // `cancelled`, setReady was skipped, and the re-run hit the loadedIdRef
// "Not in the gallery" and "not in the gallery YET" are different // guard and returned — the page hung on "Loading example…" forever
// answers while the pro overlay's dynamic import is still in flight: // (found with the reSpeaker example; any /example/<pro-id> direct URL
// a direct link to a pro example used to flash a 404 (marketing // could lose this race). The 404 decision lives in the render below,
// header and all) before the overlay registered it and the editor // where reading `settled` doesn't cancel anything.
// took over. Stay on the loading screen until the registry settles; if (!exampleId || !example) return;
// only then is a missing id really a 404.
if (settled) setError(true);
return;
}
if (loadedIdRef.current === exampleId) return; if (loadedIdRef.current === exampleId) return;
loadedIdRef.current = exampleId; loadedIdRef.current = exampleId;
let cancelled = false; let cancelled = false;
let done = false;
setReady(false); setReady(false);
setError(false); setError(false);
(async () => { (async () => {
@ -95,18 +92,29 @@ export const ExampleEditorPage: React.FC = () => {
// are swallowed inside ensureLibraries — anything that DOES bubble // are swallowed inside ensureLibraries — anything that DOES bubble
// up here means the stores are partially populated. Surfacing a // up here means the stores are partially populated. Surfacing a
// clean error is more useful than rendering an empty editor. // clean error is more useful than rendering an empty editor.
done = true;
if (!cancelled) setError(true); if (!cancelled) setError(true);
return; return;
} }
done = true;
if (!cancelled) setReady(true); if (!cancelled) setReady(true);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
// A load cancelled mid-flight must not poison the guard: if this
// effect re-runs for the same id, it has to actually reload instead
// of early-returning with `ready` still false.
if (!done && loadedIdRef.current === exampleId) {
loadedIdRef.current = null;
}
}; };
}, [exampleId, example, settled]); }, [exampleId, example]);
if (error) { // "Not in the gallery" and "not in the gallery YET" are different answers
// while the pro overlay's dynamic import is still in flight: only once the
// registry settles is a missing id really a 404.
if (error || !exampleId || (settled && !example)) {
return ( return (
<div <div
style={{ style={{