From 8840d8b8cbf1f8df09d1ee450cf429d395e7820d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papli=C5=84ski?= Date: Wed, 25 Mar 2026 21:57:57 +0100 Subject: [PATCH 1/2] fix race condition --- packages/devtools/src/core.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/devtools/src/core.ts b/packages/devtools/src/core.ts index 4096c8eb..7e082040 100644 --- a/packages/devtools/src/core.ts +++ b/packages/devtools/src/core.ts @@ -40,9 +40,8 @@ export class TanStackDevtoolsCore { ...initialState.settings, } #plugins: Array = [] - #isMounted = false - #isMounting = false - #abortMount = false + #state: 'mounted' | 'mounting' | 'unmounted' = 'unmounted' + #mountAbortController?: AbortController #dispose?: () => void #eventBus?: { stop: () => void } #eventBusConfig: ClientEventBusConfig | undefined @@ -60,16 +59,15 @@ export class TanStackDevtoolsCore { mount(el: T) { if (typeof document === 'undefined') return - if (this.#isMounted || this.#isMounting) { + if (this.#state === 'mounted' || this.#state === 'mounting') { throw new Error('Devtools is already mounted') } - this.#isMounting = true - this.#abortMount = false + this.#state = 'mounting' + const { signal } = (this.#mountAbortController = new AbortController()) import('./mount-impl') .then(({ mountDevtools }) => { - if (this.#abortMount) { - this.#isMounting = false + if (signal.aborted) { return } @@ -85,27 +83,22 @@ export class TanStackDevtoolsCore { this.#dispose = result.dispose this.#eventBus = result.eventBus - this.#isMounted = true - this.#isMounting = false + this.#state = 'mounted' }) .catch((err) => { - this.#isMounting = false + this.#state = 'unmounted' console.error('[TanStack Devtools] Failed to load:', err) }) } unmount() { - if (!this.#isMounted && !this.#isMounting) { + if (this.#state === 'unmounted') { throw new Error('Devtools is not mounted') } - if (this.#isMounting) { - this.#abortMount = true - this.#isMounting = false - return - } + this.#mountAbortController?.abort() this.#eventBus?.stop() this.#dispose?.() - this.#isMounted = false + this.#state = 'unmounted' } setConfig(config: Partial) { @@ -116,7 +109,7 @@ export class TanStackDevtoolsCore { if (config.plugins) { this.#plugins = config.plugins // Update the reactive store if mounted - if (this.#isMounted && this.#setPlugins) { + if (this.#state === 'mounted' && this.#setPlugins) { this.#setPlugins(config.plugins) } } From 8cca2892ddc3f2cd7fc602b3c80f72eac6d19a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papli=C5=84ski?= Date: Wed, 25 Mar 2026 22:04:21 +0100 Subject: [PATCH 2/2] add changeset --- .changeset/legal-trains-throw.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/legal-trains-throw.md diff --git a/.changeset/legal-trains-throw.md b/.changeset/legal-trains-throw.md new file mode 100644 index 00000000..b10f3572 --- /dev/null +++ b/.changeset/legal-trains-throw.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools': patch +--- + +Fix duplicate Devtools UI rendering when React StrictMode is enabled.