diff --git a/examples/react/time-travel/.eslintrc.cjs b/examples/react/time-travel/.eslintrc.cjs new file mode 100644 index 00000000..9ff0b9fc --- /dev/null +++ b/examples/react/time-travel/.eslintrc.cjs @@ -0,0 +1,13 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = { + settings: { + extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'], + rules: { + 'react/no-children-prop': 'off', + }, + }, +} + +module.exports = config diff --git a/examples/react/time-travel/.gitignore b/examples/react/time-travel/.gitignore new file mode 100644 index 00000000..4673b022 --- /dev/null +++ b/examples/react/time-travel/.gitignore @@ -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* diff --git a/examples/react/time-travel/README.md b/examples/react/time-travel/README.md new file mode 100644 index 00000000..1cf88926 --- /dev/null +++ b/examples/react/time-travel/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/time-travel/index.html b/examples/react/time-travel/index.html new file mode 100644 index 00000000..f24fe41d --- /dev/null +++ b/examples/react/time-travel/index.html @@ -0,0 +1,16 @@ + + + + + + + + + Basic Example - TanStack Devtools + + + +
+ + + diff --git a/examples/react/time-travel/package.json b/examples/react/time-travel/package.json new file mode 100644 index 00000000..ff855104 --- /dev/null +++ b/examples/react/time-travel/package.json @@ -0,0 +1,41 @@ +{ + "name": "@tanstack/devtools-example-react-time-travel", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/devtools-event-client": "workspace:^", + "@tanstack/react-devtools": "^0.3.0", + "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query-devtools": "^5.83.0", + "@tanstack/react-router": "^1.130.2", + "@tanstack/react-router-devtools": "^1.130.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "zod": "^4.0.14", + "zustand": "^5.0.7" + }, + "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" + ] + } +} diff --git a/examples/react/time-travel/public/emblem-light.svg b/examples/react/time-travel/public/emblem-light.svg new file mode 100644 index 00000000..a58e69ad --- /dev/null +++ b/examples/react/time-travel/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/time-travel/src/index.tsx b/examples/react/time-travel/src/index.tsx new file mode 100644 index 00000000..e569c9d1 --- /dev/null +++ b/examples/react/time-travel/src/index.tsx @@ -0,0 +1,20 @@ +import { createRoot } from 'react-dom/client' +import { useStore } from 'zustand' +import Devtools from './setup' +import { store } from './zustand-client' + +function App() { + const { count, increment, decrement } = useStore(store) + return ( +
+

Zustand time-travel

+

Current count: {count}

+ + + +
+ ) +} + +const root = createRoot(document.getElementById('root')!) +root.render() diff --git a/examples/react/time-travel/src/setup.tsx b/examples/react/time-travel/src/setup.tsx new file mode 100644 index 00000000..bcec391b --- /dev/null +++ b/examples/react/time-travel/src/setup.tsx @@ -0,0 +1,87 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/react-router' +import { TanstackDevtools } from '@tanstack/react-devtools' +import { ZustandTimeTravel } from './zustand-time-travel' + +const rootRoute = createRootRoute({ + component: () => ( + <> +
+ + Home + {' '} + + About + +
+
+ + + ), +}) + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: function Index() { + return ( +
+

Welcome Home!

+
+ ) + }, +}) +function About() { + return ( +
+

Hello from About!

+
+ ) +} + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: About, +}) + +const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]) + +const router = createRouter({ routeTree }) + +const queryClient = new QueryClient() + +export default function DevtoolsExample() { + return ( + <> + + , + }, + { + name: 'Tanstack Router', + render: , + }, + { + name: 'Zustand time-travel', + render: , + }, + ]} + /> + + + + ) +} diff --git a/examples/react/time-travel/src/zustand-client.ts b/examples/react/time-travel/src/zustand-client.ts new file mode 100644 index 00000000..404dfd75 --- /dev/null +++ b/examples/react/time-travel/src/zustand-client.ts @@ -0,0 +1,36 @@ +import { EventClient } from '@tanstack/devtools-event-client' +import { createStore } from 'zustand' + +interface ZustandEventMap { + 'zustand:stateChange': any + 'zustand:revertSnapshot': any +} +export const eventClient = new EventClient({ + pluginId: 'zustand', +}) + +export const store = createStore<{ + count: number + increment: () => void + decrement: () => void +}>((set) => ({ + count: 0, + increment: () => { + return set((state) => { + eventClient.emit('stateChange', { count: state.count + 1 }) + return { count: state.count + 1 } + }) + }, + decrement: () => { + return set((state) => { + eventClient.emit('stateChange', { count: state.count - 1 }) + return { count: state.count - 1 } + }) + }, +})) + +eventClient.on('revertSnapshot', (snapshot) => { + store.setState({ + count: snapshot.payload.count, + }) +}) diff --git a/examples/react/time-travel/src/zustand-time-travel.tsx b/examples/react/time-travel/src/zustand-time-travel.tsx new file mode 100644 index 00000000..04a20b9a --- /dev/null +++ b/examples/react/time-travel/src/zustand-time-travel.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react' +import { eventClient } from './zustand-client' + +export function ZustandTimeTravel() { + const [snapshots, setSnapshots] = useState>([]) + + useEffect(() => { + const cleanup = eventClient.on('stateChange', (event) => + setSnapshots((prev) => [...prev, event.payload]), + ) + return () => { + cleanup() + } + }, []) + + return ( +
+ {/* Snapshot slider to change the current state */} + Drag Me to time travel through zustand states +
+ { + const index = Number(e.target.value) + eventClient.emit('revertSnapshot', snapshots[index]) + }} + /> +
+ ) +} diff --git a/examples/react/time-travel/tsconfig.json b/examples/react/time-travel/tsconfig.json new file mode 100644 index 00000000..6e9088d6 --- /dev/null +++ b/examples/react/time-travel/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/react/time-travel/vite.config.ts b/examples/react/time-travel/vite.config.ts new file mode 100644 index 00000000..4e194366 --- /dev/null +++ b/examples/react/time-travel/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react({ + // babel: { + // plugins: [['babel-plugin-react-compiler', { target: '19' }]], + // }, + }), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8d14fd3..5f4a6a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,6 +242,52 @@ importers: examples/react/start/generated/prisma: {} + examples/react/time-travel: + dependencies: + '@tanstack/devtools-event-client': + specifier: workspace:^ + version: link:../../../packages/event-bus-client + '@tanstack/react-devtools': + specifier: ^0.3.0 + version: link:../../../packages/react-devtools + '@tanstack/react-query': + specifier: ^5.83.0 + version: 5.83.0(react@19.1.0) + '@tanstack/react-query-devtools': + specifier: ^5.83.0 + version: 5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-router': + specifier: ^1.130.2 + version: 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-router-devtools': + specifier: ^1.130.2 + version: 1.130.2(@tanstack/react-router@1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.130.12)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.7)(tiny-invariant@1.3.3) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + zod: + specifier: ^4.0.14 + version: 4.0.14 + zustand: + specifier: ^5.0.7 + version: 5.0.7(@types/react@19.1.2)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + devDependencies: + '@types/react': + specifier: ^19.1.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.7.0(vite@7.0.6(@types/node@22.15.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + vite: + specifier: ^7.0.6 + version: 7.0.6(@types/node@22.15.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + examples/solid/basic: dependencies: '@tanstack/solid-devtools': @@ -6898,6 +6944,24 @@ packages: zod@4.0.14: resolution: {integrity: sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==} + zustand@5.0.7: + resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8907,6 +8971,18 @@ snapshots: '@tanstack/query-core': 5.83.0 react: 19.1.0 + '@tanstack/react-router-devtools@1.130.2(@tanstack/react-router@1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.130.12)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + dependencies: + '@tanstack/react-router': 1.130.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/router-devtools-core': 1.130.2(@tanstack/router-core@1.130.12)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@tanstack/router-core' + - csstype + - solid-js + - tiny-invariant + '@tanstack/react-router-devtools@1.130.2(@tanstack/react-router@1.130.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.130.12)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: '@tanstack/react-router': 1.130.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -14213,4 +14289,10 @@ snapshots: zod@4.0.14: {} + zustand@5.0.7(@types/react@19.1.2)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + optionalDependencies: + '@types/react': 19.1.2 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + zwitch@2.0.4: {}