From acb6372211117fbcea3130963e49c8fb9d107cb9 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 13 Aug 2025 08:25:07 +0200 Subject: [PATCH 1/3] feat: add time-travel example for zustand --- examples/react/time-travel/.eslintrc.cjs | 13 +++ examples/react/time-travel/.gitignore | 27 ++++++ examples/react/time-travel/README.md | 6 ++ examples/react/time-travel/index.html | 16 ++++ examples/react/time-travel/package.json | 41 +++++++++ .../react/time-travel/public/emblem-light.svg | 13 +++ examples/react/time-travel/src/index.tsx | 20 +++++ examples/react/time-travel/src/setup.tsx | 87 +++++++++++++++++++ .../react/time-travel/src/zustand-client.ts | 36 ++++++++ .../time-travel/src/zustand-time-travel.tsx | 31 +++++++ examples/react/time-travel/tsconfig.json | 23 +++++ examples/react/time-travel/vite.config.ts | 13 +++ pnpm-lock.yaml | 85 ++++++++++++++++++ 13 files changed, 411 insertions(+) create mode 100644 examples/react/time-travel/.eslintrc.cjs create mode 100644 examples/react/time-travel/.gitignore create mode 100644 examples/react/time-travel/README.md create mode 100644 examples/react/time-travel/index.html create mode 100644 examples/react/time-travel/package.json create mode 100644 examples/react/time-travel/public/emblem-light.svg create mode 100644 examples/react/time-travel/src/index.tsx create mode 100644 examples/react/time-travel/src/setup.tsx create mode 100644 examples/react/time-travel/src/zustand-client.ts create mode 100644 examples/react/time-travel/src/zustand-time-travel.tsx create mode 100644 examples/react/time-travel/tsconfig.json create mode 100644 examples/react/time-travel/vite.config.ts 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..ca33f91f --- /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" + ] + } +} \ No newline at end of file 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..69f96dc9 --- /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..3707f9c7 --- /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..fe1b8deb --- /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..7e1a8202 --- /dev/null +++ b/examples/react/time-travel/src/zustand-time-travel.tsx @@ -0,0 +1,31 @@ +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..a9c21b89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: 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 @@ -242,6 +245,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 +6947,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 +8974,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 +14292,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: {} From 1658a65d51ff42739d8d0773e9b17a7c1e056e67 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 13 Aug 2025 08:26:45 +0200 Subject: [PATCH 2/3] fix: fix lock --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9c21b89..5f4a6a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,9 +101,6 @@ importers: 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 From c6462ebb56e762f84a2a5a6477f798cd1464b2db Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 06:27:18 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- examples/react/time-travel/package.json | 2 +- examples/react/time-travel/src/index.tsx | 2 +- examples/react/time-travel/src/setup.tsx | 4 +-- .../react/time-travel/src/zustand-client.ts | 32 +++++++++---------- .../time-travel/src/zustand-time-travel.tsx | 23 ++++++------- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/examples/react/time-travel/package.json b/examples/react/time-travel/package.json index ca33f91f..ff855104 100644 --- a/examples/react/time-travel/package.json +++ b/examples/react/time-travel/package.json @@ -38,4 +38,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/examples/react/time-travel/src/index.tsx b/examples/react/time-travel/src/index.tsx index 69f96dc9..e569c9d1 100644 --- a/examples/react/time-travel/src/index.tsx +++ b/examples/react/time-travel/src/index.tsx @@ -4,7 +4,7 @@ import Devtools from './setup' import { store } from './zustand-client' function App() { - const { count, increment, decrement } = useStore(store); + const { count, increment, decrement } = useStore(store) return (

Zustand time-travel

diff --git a/examples/react/time-travel/src/setup.tsx b/examples/react/time-travel/src/setup.tsx index 3707f9c7..bcec391b 100644 --- a/examples/react/time-travel/src/setup.tsx +++ b/examples/react/time-travel/src/setup.tsx @@ -75,9 +75,9 @@ export default function DevtoolsExample() { render: , }, { - name: "Zustand time-travel", + 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 index fe1b8deb..404dfd75 100644 --- a/examples/react/time-travel/src/zustand-client.ts +++ b/examples/react/time-travel/src/zustand-client.ts @@ -1,36 +1,36 @@ -import { EventClient } from "@tanstack/devtools-event-client"; -import { createStore, } from "zustand"; +import { EventClient } from '@tanstack/devtools-event-client' +import { createStore } from 'zustand' interface ZustandEventMap { - "zustand:stateChange": any; - "zustand:revertSnapshot": any; + 'zustand:stateChange': any + 'zustand:revertSnapshot': any } export const eventClient = new EventClient({ - pluginId: "zustand" -}); + pluginId: 'zustand', +}) export const store = createStore<{ - count: number; - increment: () => void; - decrement: () => void; + count: number + increment: () => void + decrement: () => void }>((set) => ({ count: 0, increment: () => { return set((state) => { - eventClient.emit("stateChange", { count: state.count + 1 }); + eventClient.emit('stateChange', { count: state.count + 1 }) return { count: state.count + 1 } }) }, decrement: () => { return set((state) => { - eventClient.emit("stateChange", { count: state.count - 1 }); + eventClient.emit('stateChange', { count: state.count - 1 }) return { count: state.count - 1 } }) }, -})); +})) -eventClient.on("revertSnapshot", (snapshot) => { +eventClient.on('revertSnapshot', (snapshot) => { store.setState({ - count: snapshot.payload.count - }); -}); + 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 index 7e1a8202..04a20b9a 100644 --- a/examples/react/time-travel/src/zustand-time-travel.tsx +++ b/examples/react/time-travel/src/zustand-time-travel.tsx @@ -1,16 +1,17 @@ -import { useEffect, useState } from "react"; -import { eventClient } from "./zustand-client"; +import { useEffect, useState } from 'react' +import { eventClient } from './zustand-client' export function ZustandTimeTravel() { - const [snapshots, setSnapshots] = useState>([]); + const [snapshots, setSnapshots] = useState>([]) useEffect(() => { - - const cleanup = eventClient.on("stateChange", (event) => setSnapshots((prev) => [...prev, event.payload])); + const cleanup = eventClient.on('stateChange', (event) => + setSnapshots((prev) => [...prev, event.payload]), + ) return () => { - cleanup(); - }; - }, []); + cleanup() + } + }, []) return (
@@ -22,10 +23,10 @@ export function ZustandTimeTravel() { min={0} max={snapshots.length - 1} onChange={(e) => { - const index = Number(e.target.value); - eventClient.emit("revertSnapshot", snapshots[index]); + const index = Number(e.target.value) + eventClient.emit('revertSnapshot', snapshots[index]) }} />
- ); + ) }