Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
198 changes: 198 additions & 0 deletions docs/framework/react/guides/custom-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
---
title: Custom plugins
id: custom-plugins
---

Tanstack devtools allows you to create your own custom plugins by emitting and listening to our event bus.

## Prerequisite

This guide will walk you through a simple example where our library is a counter with a count history. A working example can be found in our [custom-plugin example](https://tanstack.com/devtools/latest/docs/framework/react/examples/custom-plugin).

This is our library code:

counter.ts
```tsx
export function createCounter() {
let count = 0;
const history = [];

return {
getCount: () => count,
increment: () => {
history.push(count);
count++;
},
decrement: () => {
history.push(count);
count--;
},
};
}
```

## Event Client Setup

Install the [TanStack Devtools Event Client](https://tanstack.com/devtools/) utils.

```bash
npm i @tanstack/devtools-event-client
```

First you will need to setup the `EventClient`.

eventClient.ts
```tsx
import { EventClient } from '@tanstack/devtools-event-client'


type EventMap = {
// The key of the event map is a combination of {pluginId}:{eventSuffix}
// The value is the expected type of the event payload
'custom-devtools:counter-state': { count: number, history: number[], }
}

class CustomEventClient extends EventClient<EventMap> {
constructor() {
super({
// The pluginId must match that of the event map key
pluginId: 'custom-devtools',
})
}
}

// This is where the magic happens, it'll be used throughout your application.
export const DevtoolsEventClient = new FormEventClient()
```

## Event Client Integration

Now we need to hook our `EventClient` into out application code. This can be done in many way's, a UseEffect that emits the current state, or a subscription to an observer, all that matters is that when you want to emit the current state you do the following.

Our new library code will looks as follows:

counter.ts
```tsx
import { DevtoolsEventClient } from './eventClient.ts'

export function createCounter() {
let count = 0
const history: Array<number> = []

return {
getCount: () => count,
increment: () => {
history.push(count)

// The emit eventSuffix must match that of the EventMap defined in eventClient.ts
DevtoolsEventClient.emit('counter-state', {
count: count++,
history: history,
})
},
decrement: () => {
history.push(count)

DevtoolsEventClient.emit('counter-state', {
count: count--,
history: history,
})
},
}
}
```

> **Important** `EventClient` is framework agnostic so this process will be the same regardless of framework or even in vanilla JavaScript.

## Consuming The Event Client

Now we need to create our devtools panel, for a simple approach write the devtools in the framework that the adapter is, be aware that this will make the plugin framework specific.

> Because TanStack is framework agnostic we have taken a more complicated approach that will be explained in coming docs (if framework agnosticism is not a concern to you you can ignore this).

DevtoolsPanel.ts
```tsx
import { DevtoolsEventClient } from './eventClient.ts'

export function DevtoolPanel() {
const [state,setState] = useState();

useEffect(() => {
// subscribe to the emitted event
const cleanup = client.on("counter-state", e => setState(e.payload)
return cleanup
}, []);

return (
<div>
<div>{state.count}</div>
<div>{JSON.stringify(state.history)}</div>
<div/>
)
}
```

## Application Integration

This step follows what's shown in [../basic-setup] for a more documented guide go check it out. As well as the complete [custom-plugin example](https://tanstack.com/devtools/latest/docs/framework/react/examples/custom-plugin) in our examples section.

Main.tsx
```tsx
import { DevtoolPanel } from './DevtoolPanel'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />

<TanstackDevtools
plugins={[
{
// Name it what you like, this is how it will appear in the Menu
name: 'Custom devtools',
render: <DevtoolPanel />,
},
]}
/>
</StrictMode>,
)

```

## Debugging

Both the TansTack `TanstackDevtools` component and the TanStack `EventClient` come with built in debug mode which will log to the console the emitted event as well as the EventClient status.

TanstackDevtool's debugging mode can be activated like so:
```tsx
<TanstackDevtools
eventBusConfig={{ debug: true }}
plugins={[
{
// call it what you like, this is how it will appear in the Menu
name: 'Custom devtools',
render: <DevtoolPanel />,
},
]}
/>
```

Where as the EventClient's debug mode can be activated by:
```tsx
class CustomEventClient extends EventClient<EventMap> {
constructor() {
super({
pluginId: 'custom-devtools',
debug: true,
})
}
}
```

Activating the debug mode will log to the console the current events that emitter has emitted or listened to. The EventClient will have appended `[tanstack-devtools:${pluginId}]` and the client will have appended `[tanstack-devtools:client-bus]`.

Heres an example of both:
```
🌴 [tanstack-devtools:client-bus] Initializing client event bus

🌴 [tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state
```
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/react/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
<title>TanStack Devtools Example</title>
<title>Basic Example - TanStack Devtools</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
2 changes: 1 addition & 1 deletion examples/react/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"devDependencies": {
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"@vitejs/plugin-react": "^4.5.2",
"vite": "^7.0.6"
},
"browserslist": {
Expand Down
11 changes: 11 additions & 0 deletions examples/react/custom-devtools/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-check

/** @type {import('eslint').Linter.Config} */
const config = {
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
rules: {
'react/no-children-prop': 'off',
},
}

module.exports = config
27 changes: 27 additions & 0 deletions examples/react/custom-devtools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

pnpm-lock.yaml
yarn.lock
package-lock.json

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
6 changes: 6 additions & 0 deletions examples/react/custom-devtools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install`
- `npm run dev`
16 changes: 16 additions & 0 deletions examples/react/custom-devtools/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

<title>Custom Devtools - TanStack Devtools</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions examples/react/custom-devtools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@tanstack/devtools-custom-devtools",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"preview": "vite preview",
"test:types": "tsc"
},
"dependencies": {
"@tanstack/devtools-event-client": "https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-client@11",
"@tanstack/react-devtools": "https://pkg.pr.new/TanStack/devtools/@tanstack/react-devtools@0a0219b",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.5.2",
"vite": "^7.0.6"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
13 changes: 13 additions & 0 deletions examples/react/custom-devtools/public/emblem-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions examples/react/custom-devtools/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState } from 'react'

import { createCounter } from './counter'

const counterInstance = createCounter()

export default function App() {
const [count, setCount] = useState(counterInstance.getCount())

const increment = () => {
counterInstance.increment()
setCount(counterInstance.getCount())
}

const decrement = () => {
counterInstance.decrement()
setCount(counterInstance.getCount())
}

return (
<div>
<h1>Custom plugins</h1>
<h2>Count: {count}</h2>
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={increment}>+ increase</button>
<button onClick={decrement}>− decrease</button>
</div>
</div>
)
}
23 changes: 23 additions & 0 deletions examples/react/custom-devtools/src/CustomDevtoolsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useState } from 'react'
import { DevtoolsEventClient } from './eventClient.ts'

export default function CustomDevtoolPanel() {
const [state, setState] = useState<
{ count: number; history: Array<number> } | undefined
>()

useEffect(() => {
// subscribe to the emitted event
const cleanup = DevtoolsEventClient.on('counter-state', (e) =>
setState(e.payload),
)
return cleanup
}, [])

return (
<div>
<div>counter state: {state?.count}</div>
<div>counter history: {JSON.stringify(state?.history)}</div>
</div>
)
}
Loading