Skip to content

Commit ec60509

Browse files
committed
docs(react): custom plugin docs and custom plugin example
1 parent a367cf2 commit ec60509

File tree

14 files changed

+569
-1
lines changed

14 files changed

+569
-1
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: Custom plugins
3+
id: custom-plugins
4+
---
5+
6+
Tanstack devtools allows you to create your own custom plugins by emitting and listening to our event bus.
7+
8+
## Prerequisite
9+
10+
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).
11+
12+
This is our library code:
13+
14+
counter.ts
15+
```tsx
16+
export function createCounter() {
17+
let count = 0;
18+
const history = [];
19+
20+
return {
21+
getCount: () => count,
22+
increment: () => {
23+
history.push(count);
24+
count++;
25+
},
26+
decrement: () => {
27+
history.push(count);
28+
count--;
29+
},
30+
};
31+
}
32+
```
33+
34+
## Event Client Setup
35+
36+
Install the [TanStack Devtools Event Client](https://tanstack.com/devtools/) utils.
37+
38+
```bash
39+
npm i @tanstack/devtools-event-client
40+
```
41+
42+
First you will need to setup the `EventClient`.
43+
44+
eventClient.ts
45+
```tsx
46+
import { EventClient } from '@tanstack/devtools-event-client'
47+
48+
49+
type EventMap = {
50+
// The key of the event map is a combination of {pluginId}:{eventSuffix}
51+
// The value is the expected type of the event payload
52+
'custom-devtools:counter-state': { count: number, history: number[], }
53+
}
54+
55+
class CustomEventClient extends EventClient<EventMap> {
56+
constructor() {
57+
super({
58+
// The pluginId must match that of the event map key
59+
pluginId: 'custom-devtools',
60+
})
61+
}
62+
}
63+
64+
// This is where the magic happens, it'll be used throughout your application.
65+
export const DevtoolsEventClient = new FormEventClient()
66+
```
67+
68+
## Event Client Integration
69+
70+
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.
71+
72+
Our new library code will looks as follows:
73+
74+
counter.ts
75+
```tsx
76+
import { DevtoolsEventClient } from './eventClient.ts'
77+
78+
export function createCounter() {
79+
let count = 0;
80+
const history = [];
81+
82+
return {
83+
getCount: () => count,
84+
increment: () => {
85+
const newCount = count++
86+
history.push(count);
87+
88+
// The emit eventSuffix must match that of the EventMap defined in eventClient.ts
89+
DevtoolsEventClient.emit('counter-state', {
90+
count: newCount
91+
history: history
92+
})
93+
94+
count = newCount
95+
},
96+
decrement: () => {
97+
const newCount = count--
98+
history.push(count);
99+
100+
DevtoolsEventClient.emit('counter-state', {
101+
count: newCount
102+
history: history
103+
})
104+
105+
count = newCount
106+
},
107+
};
108+
}
109+
```
110+
111+
> **Important** `EventClient` is framework agnostic so this process will be the same regardless of framework or even vanilla in JavaScript.
112+
113+
## Consuming The Event Client
114+
115+
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.
116+
117+
> 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).
118+
119+
DevtoolsPanel.ts
120+
```tsx
121+
import { DevtoolsEventClient } from './eventClient.ts'
122+
123+
export function DevtoolPanel() {
124+
const [state,setState] = useState();
125+
126+
useEffect(() => {
127+
// subscribe to the emitted event
128+
const cleanup = client.on("counter-state", e => setState(e.payload)
129+
return cleanup
130+
}, []);
131+
132+
return (
133+
<div>
134+
<div>{state.count}</div>
135+
<div>{JSON.stringify(state.history)}</div>
136+
<div/>
137+
)
138+
}
139+
```
140+
141+
## Application Integration
142+
143+
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.
144+
145+
Main.tsx
146+
```tsx
147+
import { DevtoolPanel } from './DevtoolPanel'
148+
149+
createRoot(document.getElementById('root')!).render(
150+
<StrictMode>
151+
<App />
152+
153+
<TanstackDevtools
154+
plugins={[
155+
{
156+
name: 'Custom devtools',
157+
render: <DevtoolPanel />,
158+
},
159+
]}
160+
/>
161+
</StrictMode>,
162+
)
163+
164+
```
165+
166+
## Debugging
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @ts-check
2+
3+
/** @type {import('eslint').Linter.Config} */
4+
const config = {
5+
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
6+
rules: {
7+
'react/no-children-prop': 'off',
8+
},
9+
}
10+
11+
module.exports = config
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
pnpm-lock.yaml
15+
yarn.lock
16+
package-lock.json
17+
18+
# misc
19+
.DS_Store
20+
.env.local
21+
.env.development.local
22+
.env.test.local
23+
.env.production.local
24+
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
9+
<title>TanStack Form React Simple Example App</title>
10+
</head>
11+
<body>
12+
<noscript>You need to enable JavaScript to run this app.</noscript>
13+
<div id="root"></div>
14+
<script type="module" src="/src/index.tsx"></script>
15+
</body>
16+
</html>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@tanstack/devtools-custom-devtools",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port=3001",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "tsc"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-devtools": "https://pkg.pr.new/TanStack/devtools/@tanstack/react-devtools@0a0219b",
13+
"@tanstack/devtools-event-client": "https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-client@11",
14+
"react": "^19.0.0",
15+
"react-dom": "^19.0.0"
16+
},
17+
"devDependencies": {
18+
"@types/react": "^19.0.7",
19+
"@types/react-dom": "^19.0.3",
20+
"@vitejs/plugin-react": "^4.5.2",
21+
"vite": "^7.0.6"
22+
},
23+
"browserslist": {
24+
"production": [
25+
">0.2%",
26+
"not dead",
27+
"not op_mini all"
28+
],
29+
"development": [
30+
"last 1 chrome version",
31+
"last 1 firefox version",
32+
"last 1 safari version"
33+
]
34+
}
35+
}
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useState } from 'react'
2+
3+
import { createCounter } from './counter'
4+
5+
const counterInstance = createCounter()
6+
7+
export default function App() {
8+
const [count, setCount] = useState(counterInstance.getCount())
9+
10+
const increment = () => {
11+
counterInstance.increment()
12+
setCount(counterInstance.getCount())
13+
}
14+
15+
const decrement = () => {
16+
counterInstance.decrement()
17+
setCount(counterInstance.getCount())
18+
}
19+
20+
return (
21+
<div>
22+
<h2>Count: {count}</h2>
23+
<button onClick={increment}>+</button>
24+
<button onClick={decrement}></button>
25+
</div>
26+
)
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useEffect, useState } from 'react'
2+
import { DevtoolsEventClient } from './eventClient.ts'
3+
4+
export default function CustomDevtoolPanel() {
5+
const [state, setState] = useState<
6+
{ count: number; history: Array<number> } | undefined
7+
>()
8+
9+
useEffect(() => {
10+
// subscribe to the emitted event
11+
const cleanup = DevtoolsEventClient.on('counter-state', (e) =>
12+
setState(e.payload),
13+
)
14+
return cleanup
15+
}, [])
16+
17+
return (
18+
<div>
19+
<div>{state?.count}</div>
20+
<div>{JSON.stringify(state?.history)}</div>
21+
</div>
22+
)
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { DevtoolsEventClient } from './eventClient.ts'
2+
3+
export function createCounter() {
4+
let count = 0
5+
const history: Array<number> = []
6+
7+
return {
8+
getCount: () => count,
9+
increment: () => {
10+
const newCount = count + 1
11+
history.push(count)
12+
13+
// The emit eventSuffix must match that of the EventMap defined in eventClient.ts
14+
DevtoolsEventClient.emit('counter-state', {
15+
count: newCount,
16+
history: history,
17+
})
18+
19+
count = newCount
20+
},
21+
decrement: () => {
22+
const newCount = count - 1
23+
history.push(count)
24+
25+
DevtoolsEventClient.emit('counter-state', {
26+
count: newCount,
27+
history: history,
28+
})
29+
30+
count = newCount
31+
},
32+
}
33+
}

0 commit comments

Comments
 (0)