diff --git a/packages/overlays/package.json b/packages/overlays/package.json
index 5c61ab020..96b2c045a 100644
--- a/packages/overlays/package.json
+++ b/packages/overlays/package.json
@@ -102,7 +102,7 @@
"root": true
},
"dependencies": {
- "@sanity/visual-editing": "1.8.20"
+ "@sanity/visual-editing": "workspace:*"
},
"devDependencies": {
"@repo/package.config": "workspace:*",
diff --git a/packages/svelte-loader/package.json b/packages/svelte-loader/package.json
index 7395b24b0..9a9e63203 100644
--- a/packages/svelte-loader/package.json
+++ b/packages/svelte-loader/package.json
@@ -98,7 +98,7 @@
"@sanity/client": "^6.18.2",
"@sanity/pkg-utils": "6.8.15",
"@sanity/preview-url-secret": "^1.6.13",
- "@sanity/visual-editing": "1.8.20",
+ "@sanity/visual-editing": "workspace:*",
"@sveltejs/kit": "^2.5.8",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
diff --git a/packages/visual-editing/src/ui/enableVisualEditing.tsx b/packages/visual-editing/src/ui/enableVisualEditing.tsx
index ed9f50998..ed34f3a08 100644
--- a/packages/visual-editing/src/ui/enableVisualEditing.tsx
+++ b/packages/visual-editing/src/ui/enableVisualEditing.tsx
@@ -1,12 +1,5 @@
-import type {Root} from 'react-dom/client'
-
-import {OVERLAY_ID} from '../constants'
import type {DisableVisualEditing, VisualEditingOptions} from '../types'
-let node: HTMLElement | null = null
-let root: Root | null = null
-let cleanup: number | null = null
-
/**
* Enables Visual Editing overlay in a page with sourcemap encoding.
*
@@ -14,45 +7,26 @@ let cleanup: number | null = null
* @public
*/
export function enableVisualEditing(options: VisualEditingOptions = {}): DisableVisualEditing {
- if (cleanup) clearTimeout(cleanup)
const controller = new AbortController()
-
- // Lazy load everything needed to render the app
- Promise.all([import('react-dom/client'), import('./VisualEditing')]).then(
- ([reactClient, {VisualEditing}]) => {
- if (controller.signal.aborted) return
-
- if (!node) {
- node = document.createElement('div')
- node.id = OVERLAY_ID
- document.body.appendChild(node)
- }
-
- if (!root) {
- const {createRoot} = 'default' in reactClient ? reactClient.default : reactClient
- root = createRoot(node)
- }
-
- root.render(
- <>
-
- >,
- )
- },
- )
+ // Lazy load everything, react, the app, all of it
+ import('./renderVisualEditing').then(({renderVisualEditing}) => {
+ const {signal} = controller
+ /**
+ * Due to lazy loading it's possible for the following to happen, and is a consequence of dynamic ESM imports not being cancellable natively:
+ * 1. Userland calls `const disableVisualEditing = enableVisualEditing()` and the dynamic ESM import is started.
+ * 2. The dynamic import uses the network, and it takes a while to load.
+ * 3. The user navigates to a different page in the app that doesn't need Visual Editing, for example a login page.
+ * 4. Since the app is no longer in a state where Visual Editing is needed, the user calls `disableVisualEditing()`.
+ * 5. The dynamic import eventually resolves and this function is called.
+ * When this happens we want to skip calling `renderVisualEditing` since we know it's no longer needed.
+ */
+ if (signal.aborted) return
+
+ // Hand off to the render function with the signal, which will be subscribed to for detecting when to cancel the rendering if needed and unmount the app.
+ renderVisualEditing(signal, options)
+ })
return () => {
controller.abort()
- // Handle React StrictMode, delay cleanup for a second in case it's a rerender
- cleanup = window.setTimeout(() => {
- if (root) {
- root.unmount()
- root = null
- }
- if (node) {
- document.body.removeChild(node)
- node = null
- }
- }, 1000)
}
}
diff --git a/packages/visual-editing/src/ui/renderVisualEditing.tsx b/packages/visual-editing/src/ui/renderVisualEditing.tsx
new file mode 100644
index 000000000..000c5b17a
--- /dev/null
+++ b/packages/visual-editing/src/ui/renderVisualEditing.tsx
@@ -0,0 +1,52 @@
+/**
+ * The purpose of this file is to contain the logic for rendering the
+ * component in a way that is easy to lazy load for the `enableVisualEditing` function.
+ */
+
+import {createRoot, type Root} from 'react-dom/client'
+
+import {OVERLAY_ID} from '../constants'
+import type {VisualEditingOptions} from '../types'
+import {VisualEditing} from './VisualEditing'
+
+let node: HTMLElement | null = null
+let root: Root | null = null
+let cleanup: ReturnType | null = null
+
+export function renderVisualEditing(
+ signal: AbortSignal,
+ {history, refresh, zIndex}: VisualEditingOptions,
+): void {
+ // Cancel pending cleanups, this is useful to avoid overlays blinking as the parent app transition between URLs, or hot module reload is happening
+ if (cleanup) clearTimeout(cleanup)
+ // Setup a cleanup function listener right away, as the signal might abort in-between the next steps
+ signal.addEventListener('abort', () => {
+ // Handle React StrictMode, delay cleanup for a second in case it's a rerender
+ cleanup = setTimeout(() => {
+ if (root) {
+ root.unmount()
+ root = null
+ }
+ if (node) {
+ document.body.removeChild(node)
+ node = null
+ }
+ }, 1000)
+ })
+
+ if (!node) {
+ // eslint-disable-next-line no-warning-comments
+ // @TODO use 'sanity-visual-editing' instead of 'div'
+ node = document.createElement('div')
+ // eslint-disable-next-line no-warning-comments
+ // @TODO after the element is `sanity-visual-editing` instead of `div`, stop setting this ID
+ node.id = OVERLAY_ID
+ document.body.appendChild(node)
+ }
+
+ if (!root) {
+ root = createRoot(node)
+ }
+
+ root.render()
+}