Every TanStack Devtools plugin follows a well-defined lifecycle: it is registered, mounted into a DOM container, rendered when activated, and cleaned up when the devtools unmount. This page walks through each stage in detail.
All plugins implement the TanStackDevtoolsPlugin interface, which is the low-level contract between a plugin and the devtools core. Framework adapters (React, Vue, Solid, Preact) wrap this interface so you can work with familiar components, but under the hood every plugin is reduced to these fields:
interface TanStackDevtoolsPlugin {
id?: string
name: string | ((el: HTMLHeadingElement, theme: 'dark' | 'light') => void)
render: (el: HTMLDivElement, theme: 'dark' | 'light') => void
destroy?: (pluginId: string) => void
defaultOpen?: boolean
}
A unique identifier for the plugin. If you omit it, the core generates one from the name string (lowercased, spaces replaced with dashes, suffixed with the plugin's index). Providing an explicit id is useful when you need a stable identifier across page reloads - for example, to persist which plugins the user had open.
Displayed as the tab title in the sidebar. This can be:
// Simple string name
{ name: 'My Plugin', render: (el) => { /* ... */ } }
// Custom title via function
{
name: (el, theme) => {
el.innerHTML = `<span style="color: ${theme === 'dark' ? '#fff' : '#000'}">My Plugin</span>`
},
render: (el) => { /* ... */ }
}
The main rendering function. It receives a <div> container element and the current theme. Your job is to render your plugin UI into this container using whatever approach you prefer - raw DOM manipulation, a framework portal, or anything else.
render: (el, theme) => {
el.innerHTML = `<div class="${theme}">Hello from my plugin!</div>`
}
The render function is called:
Called when the plugin is removed from the active set (e.g., the user deactivates the tab) or when the devtools unmount entirely. It receives the pluginId as its argument. Use this for cleanup - cancelling timers, closing WebSocket connections, removing event listeners, etc.
Most plugins don't need to implement destroy because framework adapters handle cleanup automatically.
When set to true, the plugin's panel will open automatically when the devtools first load and no user preferences exist in localStorage. At most 3 plugins can be open simultaneously, so if more than 3 specify defaultOpen: true, only the first 3 are opened.
This setting does not override saved user preferences. Once a user has interacted with the devtools and their active-plugin selection is persisted, defaultOpen has no effect.
If only a single plugin is registered, it opens automatically regardless of defaultOpen.
Here is what happens when you provide plugins to the devtools:
Initialization - TanStackDevtoolsCore is instantiated with a plugins array. Each plugin is assigned an id if one is not already provided.
DOM containers are created - The core's Solid-based UI creates two DOM elements per plugin:
Title rendering - For each plugin, the core checks if name is a string or function. If it's a string, the text is set directly on the heading element. If it's a function, the function is called with the heading element and current theme.
Plugin activation - When the user clicks a plugin's tab (or the plugin is auto-opened via defaultOpen), the plugin is added to the activePlugins list. The core then calls plugin.render(container, theme) with the content <div> and the current theme.
Rendering - The container is a regular <div> element. Your plugin can render anything into it - DOM nodes, a framework component tree via portals, a canvas, an iframe, etc.
Theme changes - When the user toggles the theme in settings, render is called again with the new theme value. Your plugin should update its appearance accordingly.
You rarely interact with the raw TanStackDevtoolsPlugin interface directly. Instead, each framework adapter converts your familiar component model into the DOM-based plugin contract.
The React adapter takes your JSX element and uses createPortal to render it into the plugin's container element:
// What you write:
<TanStackDevtools
plugins={[{
name: 'My Plugin',
render: <MyPluginComponent />,
}]}
/>
// What happens internally:
// The adapter's render function calls:
// createPortal(<MyPluginComponent />, containerElement)
Your React component runs in its normal React tree with full access to hooks, context, state management, etc. It just renders into a different DOM location via the portal.
The Solid adapter uses Solid's <Portal> component to mount your JSX into the container:
// What you write:
<TanStackDevtools
plugins={[{
name: 'My Plugin',
render: <MyPluginComponent />,
}]}
/>
// What happens internally:
// The adapter wraps your component in:
// <Portal mount={containerElement}>{yourComponent}</Portal>
Since the devtools core is itself built in Solid, this is the most native integration. Your component runs inside the same Solid reactive system as the devtools shell.
The Vue adapter uses <Teleport> to render your Vue component into the container:
<!-- What you write: -->
<TanStackDevtools
:plugins="[{
name: 'My Plugin',
component: MyPluginComponent,
props: { someProp: 'value' },
}]"
/>
<!-- What happens internally: -->
<!-- The adapter renders: -->
<Teleport :to="'#plugin-container-' + pluginId">
<component :is="plugin.component" v-bind="plugin.props" :theme="theme" />
</Teleport>
Your Vue component receives the theme as a prop along with any other props you pass. It runs within the Vue app's reactivity system with full access to composition API, inject/provide, etc.
Regardless of framework, your plugin component runs in its normal framework context with full reactivity, hooks, signals, lifecycle methods, and dependency injection. It just renders into a different DOM location via portals or teleports. This means:
The framework adapter handles all the wiring between your component and the devtools container.
The devtools persist the active/visible plugin selection in localStorage under the key tanstack_devtools_state. This means that when a user opens specific plugin tabs, their selection survives page reloads.
Key behaviors:
When TanStackDevtoolsCore.unmount() is called - either explicitly or because the framework component unmounts - the following happens:
Framework adapters handle their own cleanup automatically:
You typically do not need to implement destroy unless your plugin has manual subscriptions, timers, WebSocket connections, or other resources that aren't tied to your framework's lifecycle. If all your cleanup is handled by framework hooks (like useEffect cleanup in React or onCleanup in Solid), the adapter takes care of it for you.