diff --git a/web/SitePreview.tsx b/web/SitePreview.tsx index 37b532c..c402b3c 100644 --- a/web/SitePreview.tsx +++ b/web/SitePreview.tsx @@ -185,6 +185,22 @@ export const SitePreview = ({ */ const [loaded, setLoaded] = useState(0); + /* + A remount is a NEW document, so `loaded` has to go back to nothing before it arrives. + + In an effect keyed on the token, and NOT in the iframe's ref. It was a ref callback, written inline + — which React treats as a different ref every render, so it detaches and reattaches on each one, + calling the callback with null and then the node. That reset `loaded` to 0 every render, which + re-rendered, which reset it again: the paint never survived long enough to happen and the page came + up with no controls at all. + + The lesson is the same one as `useClient` two files along: a value rebuilt every render must not be + the thing something else keys on. + */ + useEffect(() => { + setLoaded(0); + }, [reloadToken]); + useEffect(() => { if (!loaded) return; const frameDocument = frame.current?.contentDocument; @@ -244,12 +260,7 @@ export const SitePreview = ({ // The key is what makes `reloadToken` reload: a new key is a new element, so the browser // fetches the document again and `onLoad` runs, which re-injects the builder's chrome. key={reloadToken ?? 0} - // A remount is a NEW document, and the effect above must not paint into the old one on - // the strength of a `loaded` that referred to it. - ref={(node) => { - frame.current = node; - if (!node) setLoaded(0); - }} + ref={frame} // Applied on load as well as on change: the document is replaced on a reload, and the // properties go with it. onLoad={() => {