| title | Using devtools-utils |
|---|---|
| id | devtools-utils |
@tanstack/devtools-utils provides factory functions that simplify creating devtools plugins for each framework. Instead of manually wiring up render functions and no-op variants, these helpers produce correctly-typed plugin objects (and their production-safe no-op counterparts) from your components. Each framework has its own subpath export with an API tailored to that framework's conventions.
npm install @tanstack/devtools-utilsEvery panel component receives a theme prop so the panel can match the devtools shell appearance. The interface is defined per-framework in each subpath:
interface DevtoolsPanelProps {
theme?: 'light' | 'dark'
}The Vue variant additionally accepts
'system'as a theme value.
Import it from the framework-specific subpath:
// React
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react'
// Solid
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid'
// Preact
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact'
// Vue
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/vue'
// Angular
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/angular'Creates a [Plugin, NoOpPlugin] tuple from a React component and plugin metadata.
Signature:
function createReactPlugin(options: {
name: string
id?: string
defaultOpen?: boolean
Component: (props: DevtoolsPanelProps) => JSX.Element
}): readonly [Plugin, NoOpPlugin]Usage:
import { createReactPlugin } from '@tanstack/devtools-utils/react'
const [MyPlugin, NoOpPlugin] = createReactPlugin({
name: 'My Store',
id: 'my-store',
defaultOpen: false,
Component: ({ theme }) => <MyStorePanel theme={theme} />,
})The returned tuple contains two factory functions:
Plugin()-- returns a plugin object withname,id,defaultOpen, and arenderfunction that renders yourComponentwith the current theme.NoOpPlugin()-- returns a plugin object with the same metadata but arenderfunction that renders an empty fragment. Use this for production builds where you want to strip devtools out.
A common pattern for tree-shaking:
const [MyPlugin, NoOpPlugin] = createReactPlugin({ /* ... */ })
const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPluginFor library authors shipping a class-based devtools core that exposes mount(el, theme) and unmount() methods. This factory wraps that class in a React component that handles mounting into a div, passing the theme, and cleaning up on unmount.
Signature:
function createReactPanel<
TComponentProps extends DevtoolsPanelProps | undefined,
TCoreDevtoolsClass extends {
mount: (el: HTMLElement, theme: 'light' | 'dark') => void
unmount: () => void
},
>(CoreClass: new () => TCoreDevtoolsClass): readonly [Panel, NoOpPanel]Usage:
import { createReactPanel } from '@tanstack/devtools-utils/react'
class MyDevtoolsCore {
mount(el: HTMLElement, theme: 'light' | 'dark') {
// render your devtools UI into el
}
unmount() {
// cleanup
}
}
const [MyPanel, NoOpPanel] = createReactPanel(MyDevtoolsCore)
// Then use the panel component inside createReactPlugin:
const [MyPlugin, NoOpPlugin] = createReactPlugin({
name: 'My Store',
Component: MyPanel,
})The returned Panel component:
- Creates a
divwithheight: 100%and stores a ref to it. - Instantiates
CoreClasson mount and callscore.mount(el, theme). - Calls
core.unmount()on cleanup. - Re-mounts when the
themeprop changes.
NoOpPanel renders an empty fragment and does nothing.
Identical API to createReactPlugin, using Preact's JSX types. Import from @tanstack/devtools-utils/preact.
Signature:
function createPreactPlugin(options: {
name: string
id?: string
defaultOpen?: boolean
Component: (props: DevtoolsPanelProps) => JSX.Element
}): readonly [Plugin, NoOpPlugin]Usage:
import { createPreactPlugin } from '@tanstack/devtools-utils/preact'
const [MyPlugin, NoOpPlugin] = createPreactPlugin({
name: 'My Store',
id: 'my-store',
Component: ({ theme }) => <MyStorePanel theme={theme} />,
})The return value and behavior are the same as createReactPlugin -- a [Plugin, NoOpPlugin] tuple where Plugin renders your component and NoOpPlugin renders nothing.
Also available for Preact with the same class-based API as createReactPanel:
import { createPreactPanel } from '@tanstack/devtools-utils/preact'
const [MyPanel, NoOpPanel] = createPreactPanel(MyDevtoolsCore)Same option-object API as React and Preact, using Solid's JSX types. Import from @tanstack/devtools-utils/solid.
Signature:
function createSolidPlugin(options: {
name: string
id?: string
defaultOpen?: boolean
Component: (props: DevtoolsPanelProps) => JSX.Element
}): readonly [Plugin, NoOpPlugin]Usage:
import { createSolidPlugin } from '@tanstack/devtools-utils/solid'
const [MyPlugin, NoOpPlugin] = createSolidPlugin({
name: 'My Store',
id: 'my-store',
Component: (props) => <MyStorePanel theme={props.theme} />,
})Solid also provides a class-based panel factory. It uses createSignal and onMount/onCleanup instead of React hooks:
import { createSolidPanel } from '@tanstack/devtools-utils/solid'
const [MyPanel, NoOpPanel] = createSolidPanel(MyDevtoolsCore)The Vue factory has a different API from the JSX-based frameworks. It takes a name string and a Vue DefineComponent as separate arguments rather than an options object.
Signature:
function createVuePlugin<TComponentProps extends Record<string, any>>(
name: string,
component: DefineComponent<TComponentProps, {}, unknown>,
): readonly [Plugin, NoOpPlugin]Usage:
import { createVuePlugin } from '@tanstack/devtools-utils/vue'
import MyStorePanel from './MyStorePanel.vue'
const [MyPlugin, NoOpPlugin] = createVuePlugin('My Store', MyStorePanel)The returned functions differ from the JSX-based variants:
Plugin(props)-- returns{ name, component, props }wherecomponentis your Vue component.NoOpPlugin(props)-- returns{ name, component: Fragment, props }where the component is Vue's built-inFragment(renders nothing visible).
Both accept props that get forwarded to the component.
For class-based devtools cores, Vue provides createVuePanel. It creates a Vue defineComponent that handles mounting and unmounting the core class:
import { createVuePanel } from '@tanstack/devtools-utils/vue'
const [MyPanel, NoOpPanel] = createVuePanel(MyDevtoolsCore)The panel component accepts theme and devtoolsProps as props. It mounts the core instance into a div element on onMounted and calls unmount() on onUnmounted.
The Angular factory takes a name string and an Angular component class as separate arguments, similar to the Vue API.
Signature:
function createAngularPlugin(
name: string,
component: Type<any>,
): readonly [Plugin, NoOpPlugin]Usage:
import { createAngularPlugin } from '@tanstack/devtools-utils/angular'
import { MyStorePanelComponent } from './my-store-panel.component'
const [MyPlugin, NoOpPlugin] = createAngularPlugin('My Store', MyStorePanelComponent)The returned functions:
Plugin(inputs?)-- returns{ name, component, inputs }wherecomponentis your Angular component class.NoOpPlugin(inputs?)-- returns{ name, component: NoOpComponent, inputs }where the component is an empty standalone component (renders nothing visible).
Both accept an optional inputs object that gets forwarded to the component via setInput().
For class-based devtools cores, Angular provides createAngularPanel. It creates a standalone Angular component that handles mounting and unmounting the core class:
import { createAngularPanel } from '@tanstack/devtools-utils/angular'
const [MyPanel, NoOpPanel] = createAngularPanel(MyDevtoolsCore)The panel component accepts theme and devtoolsProps as signal inputs. It mounts the core instance into a div element using afterNextRender and calls unmount() via DestroyRef.onDestroy.
Use the factories when you are building a reusable library plugin that will be published as a package. The factories ensure:
- Consistent plugin object shape across frameworks.
- A matching
NoOpPluginvariant for production tree-shaking. - Correct typing without manual type annotations.
Use manual plugin objects when you are building a one-off internal devtools panel for your application. In that case, passing name and render directly to the devtools configuration is simpler and avoids the extra abstraction:
// Manual approach -- fine for one-off panels
{
name: 'App State',
render: (el, theme) => <MyPanel theme={theme} />,
}The factory approach becomes more valuable as you add id, defaultOpen, and need both a development and production variant of the same plugin.