From 159417c9270e74e7717810da1f2d822f0e9b5b18 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 31 Jul 2026 04:22:52 +0200 Subject: [PATCH] fix(examples): direct pro-example URLs no longer hang on Loading example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The load effect listed the overlay's `settled` flag as a dependency. On a direct /example/ 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. --- frontend/src/pages/ExampleEditorPage.tsx | 40 ++++++++++++++---------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/ExampleEditorPage.tsx b/frontend/src/pages/ExampleEditorPage.tsx index db010f86..dd234ab5 100644 --- a/frontend/src/pages/ExampleEditorPage.tsx +++ b/frontend/src/pages/ExampleEditorPage.tsx @@ -67,24 +67,21 @@ export const ExampleEditorPage: React.FC = () => { ); useEffect(() => { - if (!exampleId) { - setError(true); - return; - } - if (!example) { - // "Not in the gallery" and "not in the gallery YET" are different - // answers while the pro overlay's dynamic import is still in flight: - // a direct link to a pro example used to flash a 404 (marketing - // header and all) before the overlay registered it and the editor - // took over. Stay on the loading screen until the registry settles; - // only then is a missing id really a 404. - if (settled) setError(true); - return; - } + // `settled` is deliberately NOT a dependency. A direct link to a pro + // example can begin loading the moment the overlay registers it — one + // microtask BEFORE the overlay's import promise settles. With `settled` + // in the deps, that flip re-fired the effect mid-load: the cleanup set + // `cancelled`, setReady was skipped, and the re-run hit the loadedIdRef + // guard and returned — the page hung on "Loading example…" forever + // (found with the reSpeaker example; any /example/ direct URL + // could lose this race). The 404 decision lives in the render below, + // where reading `settled` doesn't cancel anything. + if (!exampleId || !example) return; if (loadedIdRef.current === exampleId) return; loadedIdRef.current = exampleId; let cancelled = false; + let done = false; setReady(false); setError(false); (async () => { @@ -95,18 +92,29 @@ export const ExampleEditorPage: React.FC = () => { // are swallowed inside ensureLibraries — anything that DOES bubble // up here means the stores are partially populated. Surfacing a // clean error is more useful than rendering an empty editor. + done = true; if (!cancelled) setError(true); return; } + done = true; if (!cancelled) setReady(true); })(); return () => { 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 (