@buoy-gg/zustand 5.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,138 +1,66 @@
1
1
  # @buoy-gg/zustand
2
2
 
3
- Zustand store DevTools for React Native. Live state change monitoring, state inspection, and diff visualization for your Zustand stores.
3
+ [![npm version](https://img.shields.io/npm/v/@buoy-gg/zustand?style=flat-square&labelColor=1c1c1c&color=10B981)](https://www.npmjs.com/package/@buoy-gg/zustand) [![npm downloads](https://img.shields.io/npm/dm/@buoy-gg/zustand?style=flat-square&labelColor=1c1c1c&color=10B981)](https://www.npmjs.com/package/@buoy-gg/zustand)
4
4
 
5
- ## Setup
5
+ **Zustand devtools on the device — state diffs, jump-to-state, and one-tap store reset with no middleware.**
6
+
7
+ Part of [Buoy](https://github.com/Buoy-gg/buoy) — devtools that live inside your React Native app. Install it and it auto-appears in the floating menu from [`@buoy-gg/core`](https://www.npmjs.com/package/@buoy-gg/core).
8
+
9
+ ## Install
6
10
 
7
11
  ```bash
8
- npm install @buoy-gg/zustand
12
+ npm install @buoy-gg/core @buoy-gg/zustand
9
13
  ```
10
14
 
11
- ### One line — zero store modifications
15
+ ## Quick start
16
+
17
+ Pass your stores to `FloatingDevTools` via the `zustandStores` prop — a named map of your store hooks. No middleware, no store changes; observation is subscribe-only, so your stores stay untouched.
12
18
 
13
19
  ```tsx
14
- import { watchStores } from '@buoy-gg/zustand';
20
+ import { FloatingDevTools } from '@buoy-gg/core';
15
21
  import { useCounterStore } from './stores/counter';
16
22
  import { useAuthStore } from './stores/auth';
17
23
  import { useCartStore } from './stores/cart';
18
24
 
19
- // Call once at module scope (e.g. in _layout.tsx or App.tsx):
20
- watchStores({
21
- counterStore: useCounterStore,
22
- authStore: useAuthStore,
23
- cartStore: useCartStore,
24
- });
25
+ export default function App() {
26
+ return (
27
+ <>
28
+ <YourApp />
29
+ <FloatingDevTools
30
+ zustandStores={{
31
+ counterStore: useCounterStore,
32
+ authStore: useAuthStore,
33
+ cartStore: useCartStore,
34
+ }}
35
+ />
36
+ </>
37
+ );
38
+ }
25
39
  ```
26
40
 
27
- That's it. The Zustand tool appears automatically in FloatingDevTools.
28
-
29
- Your stores stay completely untouched — `watchStores` uses Zustand's built-in `.subscribe()` to observe changes from the outside. It never modifies `setState` or any store internals.
41
+ Prefer to wire it outside React? `watchStores({ counterStore: useCounterStore })` from `@buoy-gg/zustand` does the same thing at module scope.
30
42
 
31
- ### Safe by design
43
+ ## What you get
32
44
 
33
- - **Add in seconds** — one import, one function call
34
- - **Remove in seconds** — delete those two lines, done
35
- - **Cannot break your stores** — all listener code is wrapped in try/catch. Even if our code has a bug, your stores keep working normally
36
- - **No monkey-patching** — we call `.subscribe()`, the same API your own components use
45
+ - **Every state change, captured** — store name, update type (`setState`, `replace`, `persist`, `initial`), changed keys, and a diff summary
46
+ - **Side-by-side diffs** — additions, removals, and modifications highlighted, in tree or split view
47
+ - **Jump to state** — restore any store to a previously captured state instantly
48
+ - **One-tap reset** — send any store back to its initial state without restarting the app
49
+ - **Persist awareness** — stores using Zustand's `persist` middleware are auto-detected and tagged
50
+ - **Store color coding** — each store keeps a consistent color across the UI for easy tracking
51
+ - **Search & filter** — find changes by store name or changed keys, or show only updates that modified state
52
+ - **Optional `buoyDevTools()` middleware** — wrap individual stores for partial-state capture and per-update timing (flags updates over the 16ms frame budget)
37
53
 
38
- ### Cleanup (optional)
54
+ ## Desktop & AI
39
55
 
40
- ```tsx
41
- const cleanup = watchStores({ ... });
56
+ The same live session streams to [Buoy Desktop](https://github.com/Buoy-gg/Buoy-Desktop) (free, macOS/Windows/Linux) and to Claude Code or Cursor via the [Buoy MCP server](https://buoy.gg/buoy/latest/docs/mcp).
42
57
 
43
- // Later, to stop watching:
44
- cleanup();
45
- ```
58
+ ## Free vs Pro
46
59
 
47
- ## Advanced: middleware mode
60
+ Every tool is free. [Pro](https://buoy.gg/pricing) unlocks production builds, the MCP server, and unlimited capture. Every weekend, Pro features unlock free for everyone.
48
61
 
49
- If you want extra detail (the exact partial passed to `setState` and precise timing), you can opt into the middleware approach per-store:
62
+ ---
50
63
 
51
- ```tsx
52
- import { create } from 'zustand';
53
- import { buoyDevTools } from '@buoy-gg/zustand';
54
-
55
- const useCounterStore = create(
56
- buoyDevTools(
57
- (set) => ({
58
- count: 0,
59
- increment: () => set((s) => ({ count: s.count + 1 })),
60
- }),
61
- { name: 'counterStore' }
62
- )
63
- );
64
- ```
64
+ 📚 [Full docs](https://buoy.gg/buoy/latest/docs/tools/zustand) · [All Buoy tools](https://github.com/Buoy-gg/buoy)
65
65
 
66
- This wraps `setState` internally, so it captures what was passed and how long it took. Works with middleware chaining (persist, immer, etc.):
67
-
68
- ```tsx
69
- import { persist } from 'zustand/middleware';
70
-
71
- const useAuthStore = create(
72
- buoyDevTools(
73
- persist(
74
- (set) => ({ user: null, login: (u) => set({ user: u }) }),
75
- { name: 'auth-storage' }
76
- ),
77
- { name: 'authStore' }
78
- )
79
- );
80
- ```
81
-
82
- ## Features
83
-
84
- - **Live state change capture** — every state update is recorded with prev/next state
85
- - **State inspection** — full JSON viewer for state after each change
86
- - **State diffing** — tree view and split view comparing previous vs. next state
87
- - **Changed keys tracking** — see exactly which top-level keys changed per update
88
- - **Store color coding** — each store gets a consistent color for easy visual tracking
89
- - **Performance monitoring** — updates slower than 16ms are flagged (middleware mode)
90
- - **Persist awareness** — auto-detects stores using Zustand's persist middleware
91
- - **Search & filter** — search by store name or changed keys, filter to only state-changing updates
92
- - **Jump to state** — restore any store to a previously captured state
93
- - **Reset store** — reset any store to its initial state
94
- - **Copy to clipboard** — copy state data (Pro)
95
-
96
- ## Comparison
97
-
98
- | | `watchStores()` | `buoyDevTools()` middleware |
99
- |---|---|---|
100
- | Setup | One line, no store changes | Wrap each store's `create()` |
101
- | Risk | Zero — uses subscribe only | Low — wraps setState |
102
- | Captures partial | No | Yes |
103
- | Captures timing | No | Yes |
104
- | State diff | Yes | Yes |
105
- | Changed keys | Yes | Yes |
106
- | Persist detection | Yes | Yes |
107
-
108
- ## Exports
109
-
110
- ```tsx
111
- // Primary — non-intrusive
112
- import { watchStores } from '@buoy-gg/zustand';
113
-
114
- // Advanced — middleware
115
- import { buoyDevTools } from '@buoy-gg/zustand';
116
-
117
- // Preset
118
- import { zustandToolPreset, createZustandTool } from '@buoy-gg/zustand';
119
-
120
- // Hook
121
- import { useZustandStateChanges } from '@buoy-gg/zustand';
122
-
123
- // Components (for custom UIs)
124
- import {
125
- ZustandModal,
126
- ZustandStateChangeItem,
127
- ZustandStateDetailContent,
128
- ZustandIcon,
129
- } from '@buoy-gg/zustand';
130
-
131
- // Types
132
- import type {
133
- ZustandStateChange,
134
- ZustandStoreInfo,
135
- ZustandFilter,
136
- StateChangeCategory,
137
- } from '@buoy-gg/zustand';
138
- ```
66
+ Proprietary software. © Buoy LLC. [Terms](https://buoy.gg/terms)
@@ -380,10 +380,10 @@ function ZustandModal({
380
380
  contentContainerStyle: styles.listContent,
381
381
  showsVerticalScrollIndicator: true,
382
382
  removeClippedSubviews: true,
383
- initialNumToRender: 15,
384
- maxToRenderPerBatch: 10,
385
- windowSize: 10,
386
- scrollEnabled: false
383
+ initialNumToRender: 12,
384
+ maxToRenderPerBatch: 8,
385
+ windowSize: 5,
386
+ updateCellsBatchingPeriod: 50
387
387
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
388
388
  style: styles.emptyState,
389
389
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
@@ -444,10 +444,10 @@ function ZustandModal({
444
444
  contentContainerStyle: styles.listContent,
445
445
  showsVerticalScrollIndicator: true,
446
446
  removeClippedSubviews: true,
447
- initialNumToRender: 15,
448
- maxToRenderPerBatch: 10,
449
- windowSize: 10,
450
- scrollEnabled: false
447
+ initialNumToRender: 12,
448
+ maxToRenderPerBatch: 8,
449
+ windowSize: 5,
450
+ updateCellsBatchingPeriod: 50
451
451
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EventsEmptyState, {
452
452
  isEnabled: isEnabled
453
453
  })]
@@ -462,6 +462,12 @@ function ZustandModal({
462
462
  onIndexChange: handleIndexChange
463
463
  }) : null;
464
464
  const footerHeight = selectedChange && activeChanges.length > 1 ? 68 : 0;
465
+
466
+ // The change lists own their scrolling so FlatList virtualization works
467
+ // (inside JsModal's ScrollView wrapper every row mounts — the Network
468
+ // tool measured 17 JS FPS / 133% CPU at max load from this exact
469
+ // pattern; see tool-bench batches 2026-07-06).
470
+ const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryStore != null || activeTab === "events");
465
471
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
466
472
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.JsModal, {
467
473
  visible: visible,
@@ -476,6 +482,7 @@ function ZustandModal({
476
482
  enablePersistence: true,
477
483
  initialMode: "bottomSheet",
478
484
  enableGlitchEffects: true,
485
+ disableScrollWrapper: listOwnsScroll,
479
486
  styles: {},
480
487
  footer: footerNode,
481
488
  footerHeight: footerHeight,
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.ZustandStateChangeItem = ZustandStateChangeItem;
6
+ exports.ZustandStateChangeItem = void 0;
7
+ var _react = require("react");
7
8
  var _reactNative = require("react-native");
8
9
  var _sharedUi = require("@buoy-gg/shared-ui");
9
10
  var _zustandStateStore = require("../utils/zustandStateStore");
@@ -88,7 +89,11 @@ function getPrimaryText(change) {
88
89
  }
89
90
  return change.partialPreview || "setState()";
90
91
  }
91
- function ZustandStateChangeItem({
92
+
93
+ // Memo justified by profiling (tool-bench 2026-07-06): unmemoized, every
94
+ // visible row re-rendered on each store emission. Leaf w/ stable props
95
+ // ({change, onPress} — onPress is a stable useCallback in the modal).
96
+ const ZustandStateChangeItem = exports.ZustandStateChangeItem = /*#__PURE__*/(0, _react.memo)(function ZustandStateChangeItem({
92
97
  change,
93
98
  onPress
94
99
  }) {
@@ -97,7 +102,6 @@ function ZustandStateChangeItem({
97
102
  const sublabel = getSublabel(change);
98
103
  const primaryText = getPrimaryText(change);
99
104
  const badgeText = getBadgeText(change);
100
- const relativeTime = (0, _sharedUi.useRelativeTime)(change.timestamp);
101
105
  let customBadge = undefined;
102
106
  if (change.isSlowUpdate) {
103
107
  customBadge = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -137,14 +141,16 @@ function ZustandStateChangeItem({
137
141
  statusLabel: statusLabel,
138
142
  statusSublabel: sublabel,
139
143
  primaryText: primaryText,
140
- bottomRightText: relativeTime,
144
+ bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
145
+ timestamp: change.timestamp
146
+ }),
141
147
  customBadge: customBadge,
142
148
  badgeText: customBadge ? undefined : badgeText,
143
149
  badgeColor: statusColor,
144
150
  showChevron: true,
145
151
  onPress: () => onPress(change)
146
152
  });
147
- }
153
+ });
148
154
  const styles = _reactNative.StyleSheet.create({
149
155
  badgeContainer: {
150
156
  flexDirection: "row",
@@ -376,10 +376,10 @@ export function ZustandModal({
376
376
  contentContainerStyle: styles.listContent,
377
377
  showsVerticalScrollIndicator: true,
378
378
  removeClippedSubviews: true,
379
- initialNumToRender: 15,
380
- maxToRenderPerBatch: 10,
381
- windowSize: 10,
382
- scrollEnabled: false
379
+ initialNumToRender: 12,
380
+ maxToRenderPerBatch: 8,
381
+ windowSize: 5,
382
+ updateCellsBatchingPeriod: 50
383
383
  }) : /*#__PURE__*/_jsxs(View, {
384
384
  style: styles.emptyState,
385
385
  children: [/*#__PURE__*/_jsx(Box, {
@@ -440,10 +440,10 @@ export function ZustandModal({
440
440
  contentContainerStyle: styles.listContent,
441
441
  showsVerticalScrollIndicator: true,
442
442
  removeClippedSubviews: true,
443
- initialNumToRender: 15,
444
- maxToRenderPerBatch: 10,
445
- windowSize: 10,
446
- scrollEnabled: false
443
+ initialNumToRender: 12,
444
+ maxToRenderPerBatch: 8,
445
+ windowSize: 5,
446
+ updateCellsBatchingPeriod: 50
447
447
  }) : /*#__PURE__*/_jsx(EventsEmptyState, {
448
448
  isEnabled: isEnabled
449
449
  })]
@@ -458,6 +458,12 @@ export function ZustandModal({
458
458
  onIndexChange: handleIndexChange
459
459
  }) : null;
460
460
  const footerHeight = selectedChange && activeChanges.length > 1 ? 68 : 0;
461
+
462
+ // The change lists own their scrolling so FlatList virtualization works
463
+ // (inside JsModal's ScrollView wrapper every row mounts — the Network
464
+ // tool measured 17 JS FPS / 133% CPU at max load from this exact
465
+ // pattern; see tool-bench batches 2026-07-06).
466
+ const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryStore != null || activeTab === "events");
461
467
  return /*#__PURE__*/_jsxs(TickProvider, {
462
468
  children: [/*#__PURE__*/_jsx(JsModal, {
463
469
  visible: visible,
@@ -472,6 +478,7 @@ export function ZustandModal({
472
478
  enablePersistence: true,
473
479
  initialMode: "bottomSheet",
474
480
  enableGlitchEffects: true,
481
+ disableScrollWrapper: listOwnsScroll,
475
482
  styles: {},
476
483
  footer: footerNode,
477
484
  footerHeight: footerHeight,
@@ -6,8 +6,9 @@
6
6
  * Mirrors ReduxActionItem.tsx - uses shared CompactRow for consistent styling
7
7
  */
8
8
 
9
+ import { memo } from "react";
9
10
  import { View, Text, StyleSheet } from "react-native";
10
- import { CompactRow, buoyColors, Zap, Clock, useRelativeTime } from "@buoy-gg/shared-ui";
11
+ import { CompactRow, buoyColors, Zap, Clock, RelativeTime } from "@buoy-gg/shared-ui";
11
12
  import { zustandStateStore } from "../utils/zustandStateStore";
12
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
14
  /**
@@ -84,7 +85,11 @@ function getPrimaryText(change) {
84
85
  }
85
86
  return change.partialPreview || "setState()";
86
87
  }
87
- export function ZustandStateChangeItem({
88
+
89
+ // Memo justified by profiling (tool-bench 2026-07-06): unmemoized, every
90
+ // visible row re-rendered on each store emission. Leaf w/ stable props
91
+ // ({change, onPress} — onPress is a stable useCallback in the modal).
92
+ export const ZustandStateChangeItem = /*#__PURE__*/memo(function ZustandStateChangeItem({
88
93
  change,
89
94
  onPress
90
95
  }) {
@@ -93,7 +98,6 @@ export function ZustandStateChangeItem({
93
98
  const sublabel = getSublabel(change);
94
99
  const primaryText = getPrimaryText(change);
95
100
  const badgeText = getBadgeText(change);
96
- const relativeTime = useRelativeTime(change.timestamp);
97
101
  let customBadge = undefined;
98
102
  if (change.isSlowUpdate) {
99
103
  customBadge = /*#__PURE__*/_jsxs(View, {
@@ -133,14 +137,16 @@ export function ZustandStateChangeItem({
133
137
  statusLabel: statusLabel,
134
138
  statusSublabel: sublabel,
135
139
  primaryText: primaryText,
136
- bottomRightText: relativeTime,
140
+ bottomRightText: /*#__PURE__*/_jsx(RelativeTime, {
141
+ timestamp: change.timestamp
142
+ }),
137
143
  customBadge: customBadge,
138
144
  badgeText: customBadge ? undefined : badgeText,
139
145
  badgeColor: statusColor,
140
146
  showChevron: true,
141
147
  onPress: () => onPress(change)
142
148
  });
143
- }
149
+ });
144
150
  const styles = StyleSheet.create({
145
151
  badgeContainer: {
146
152
  flexDirection: "row",
@@ -1 +1 @@
1
- {"version":3,"file":"ZustandModal.d.ts","sourceRoot":"","sources":["../../../../src/zustand/components/ZustandModal.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2CH,OAAO,KAAK,EAAE,iBAAiB,EAAsB,MAAM,UAAU,CAAC;AAqBtE,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,GACpC,EAAE,iBAAiB,sCAuhBnB"}
1
+ {"version":3,"file":"ZustandModal.d.ts","sourceRoot":"","sources":["../../../../src/zustand/components/ZustandModal.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2CH,OAAO,KAAK,EAAE,iBAAiB,EAAsB,MAAM,UAAU,CAAC;AAqBtE,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,GACpC,EAAE,iBAAiB,sCAiiBnB"}
@@ -8,6 +8,6 @@ interface ZustandStateChangeItemProps {
8
8
  change: ZustandStateChange;
9
9
  onPress: (change: ZustandStateChange) => void;
10
10
  }
11
- export declare function ZustandStateChangeItem({ change, onPress, }: ZustandStateChangeItemProps): import("react").JSX.Element;
11
+ export declare const ZustandStateChangeItem: import("react").NamedExoticComponent<ZustandStateChangeItemProps>;
12
12
  export {};
13
13
  //# sourceMappingURL=ZustandStateChangeItem.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ZustandStateChangeItem.d.ts","sourceRoot":"","sources":["../../../../src/zustand/components/ZustandStateChangeItem.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAuB,MAAM,UAAU,CAAC;AAGxE,UAAU,2BAA2B;IACnC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC/C;AAwFD,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,GACR,EAAE,2BAA2B,+BAgD7B"}
1
+ {"version":3,"file":"ZustandStateChangeItem.d.ts","sourceRoot":"","sources":["../../../../src/zustand/components/ZustandStateChangeItem.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAuB,MAAM,UAAU,CAAC;AAGxE,UAAU,2BAA2B;IACnC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC/C;AA2FD,eAAO,MAAM,sBAAsB,mEAkDjC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/zustand",
3
- "version": "5.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "Zustand store DevTools for React Native",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,8 +26,8 @@
26
26
  ],
27
27
  "sideEffects": false,
28
28
  "dependencies": {
29
- "@buoy-gg/shared-ui": "5.0.0",
30
- "@buoy-gg/license": "5.0.0"
29
+ "@buoy-gg/shared-ui": "6.0.1",
30
+ "@buoy-gg/license": "6.0.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": "*",