TanStack/query · error

Devtools is already mounted

Error message

Devtools is already mounted

What it means

Thrown by `TanstackQueryDevtools.mount()` (the non-framework, framework-agnostic devtools class) when `mount` is called on an instance that is already mounted. The class tracks a single `#isMounted` flag and renders into exactly one DOM element via Solid's `render`; calling `mount` twice would create a leaked Solid disposal tree and a duplicate panel, so it throws to fail fast.

Source

Thrown at packages/query-devtools/src/TanstackQueryDevtools.tsx:95

  setInitialIsOpen(isOpen: boolean) {
    this.#initialIsOpen[1](isOpen)
  }

  setErrorTypes(errorTypes: Array<DevtoolsErrorType>) {
    this.#errorTypes[1](errorTypes)
  }

  setClient(client: QueryClient) {
    this.#client[1](client)
  }

  setTheme(theme?: Theme) {
    this.#theme[1](theme)
  }

  mount<T extends HTMLElement>(el: T) {
    if (this.#isMounted) {
      throw new Error('Devtools is already mounted')
    }
    const dispose = render(() => {
      const [btnPosition] = this.#buttonPosition
      const [pos] = this.#position
      const [isOpen] = this.#initialIsOpen
      const [errors] = this.#errorTypes
      const [hideDisabledQueries] = this.#hideDisabledQueries
      const [queryClient] = this.#client
      const [theme] = this.#theme
      let Devtools: DevtoolsComponentType

      if (this.#Component) {
        Devtools = this.#Component
      } else {
        Devtools = lazy(() => import('./DevtoolsComponent'))
        this.#Component = Devtools
      }

View on GitHub (pinned to 159982c80b)

Solutions

  1. Call `unmount()` in the same lifecycle's cleanup before `mount()` can re-run, e.g. `useEffect(() => { devtools.mount(el); return () => devtools.unmount() }, [])`.
  2. Guard the call yourself: `if (!devtoolsMounted) { devtools.mount(el); devtoolsMounted = true }`.
  3. Create a fresh `new TanstackQueryDevtools(config)` instance for each mount target instead of reusing one.
  4. For React, prefer the `<ReactQueryDevtools />` component over the vanilla class — it manages mount/unmount for you.

Example fix

// before - double mount in StrictMode
useEffect(() => {
  devtools.mount(el)
}, [])

// after - pair mount with unmount
useEffect(() => {
  devtools.mount(el)
  return () => devtools.unmount()
}, [])
Defensive patterns

Strategy: validation

Validate before calling

// Track mount state alongside the devtools instance
let devtoolsMounted = false
const ensureMounted = (devtools, el) => {
  if (devtoolsMounted) return
  devtools.mount(el)
  devtoolsMounted = true
}

Type guard

// If you mirror the class, expose its flag instead of guessing
type MountedDevtools = { mount(el: HTMLElement): void; unmount(): void }
const isMountable = (d: MountedDevtools & { isMounted?: boolean }) =>
  !(d as { __mounted?: boolean }).__mounted

Try / catch

try {
  devtools.mount(el)
} catch (e) {
  if (e.message === 'Devtools is already mounted') {
    // already mounted, nothing to do
  } else throw e
}

Prevention

When it happens

Trigger: Calling `devtools.mount(el)` twice on the same `TanstackQueryDevtools` instance without an intervening `unmount()`. Common in React StrictMode (which double-invokes effects in dev) when the devtools is instantiated outside the effect and `mount` is called in the effect body, or when a micro-frontend / widget host remounts the devtools on route change without disposing.

Common situations: React 18 StrictMode double-mounting effects; HMR reloading a module that holds a long-lived devtools instance; manually integrating the vanilla devtools class into a lifecycle that re-runs (e.g. inside `useEffect` without an unmount cleanup, or inside a `connectedCallback` that fires twice).

Related errors


AI-assisted analysis of TanStack/query@159982c80b (2026-08-12). Data as JSON: /api/errors/f12920b88a410ee1. Report an issue: GitHub.