totally-tubular 0.4.0 → 0.5.0
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 +81 -36
- package/dist/react.d.ts +63 -0
- package/dist/react.js +80 -15
- package/dist/types.d.ts +2 -2
- package/package.json +16 -12
package/README.md
CHANGED
|
@@ -140,6 +140,11 @@ setCount((prev) => (prev ?? 0) + 1);
|
|
|
140
140
|
`useTubular` automatically unsubscribes when the component unmounts, so you
|
|
141
141
|
don't need to manage cleanup yourself.
|
|
142
142
|
|
|
143
|
+
`useTubular` is built on React's `useSyncExternalStore`, so it works with
|
|
144
|
+
server-side rendering: on the server it renders the store's current value
|
|
145
|
+
without subscribing, and on the client it subscribes during hydration. The
|
|
146
|
+
React bindings require React 18 or later.
|
|
147
|
+
|
|
143
148
|
### Resetting from React
|
|
144
149
|
|
|
145
150
|
Import `useTubularReset` from `totally-tubular/react`. It returns a stable
|
|
@@ -158,6 +163,44 @@ function ResetButton() {
|
|
|
158
163
|
The callback identity never changes, so it's safe to pass directly as an event
|
|
159
164
|
handler or as a dependency to `useEffect`.
|
|
160
165
|
|
|
166
|
+
### Binding hooks to a store
|
|
167
|
+
|
|
168
|
+
Import `createUseTubular` and `createUseTubularReset` from
|
|
169
|
+
`totally-tubular/react` to create hooks hard-locked to a single `Tubular`
|
|
170
|
+
instance, so you don't have to pass the store around at every call site.
|
|
171
|
+
|
|
172
|
+
Call them once, outside your components — they aren't hooks themselves, so
|
|
173
|
+
module scope is safe — and name what they return with a `use` prefix so
|
|
174
|
+
React's rules-of-hooks linting treats it as a hook.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { createUseTubular, createUseTubularReset } from "totally-tubular/react";
|
|
178
|
+
|
|
179
|
+
// Using the `store` created in the example above
|
|
180
|
+
const useAppState = createUseTubular(store);
|
|
181
|
+
const useAppReset = createUseTubularReset(store);
|
|
182
|
+
|
|
183
|
+
function Counter() {
|
|
184
|
+
const [count, setCount] = useAppState("count");
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<button onClick={() => setCount((n) => (n ?? 0) + 1)}>
|
|
188
|
+
Clicked {count} times
|
|
189
|
+
</button>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ResetButton() {
|
|
194
|
+
const handleReset = useAppReset();
|
|
195
|
+
return <button onClick={handleReset}>Reset</button>;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The returned hooks behave exactly like `useTubular` and `useTubularReset`
|
|
200
|
+
minus the store argument. Key paths stay fully type-checked against the bound
|
|
201
|
+
store's state shape, and each hook is scoped to exactly one instance — bound
|
|
202
|
+
hooks from different stores never cross-talk.
|
|
203
|
+
|
|
161
204
|
## Things you should know
|
|
162
205
|
|
|
163
206
|
- **Arrays and nested objects are observed at their path, not their contents.**
|
|
@@ -168,53 +211,55 @@ handler or as a dependency to `useEffect`.
|
|
|
168
211
|
|
|
169
212
|
## Performance benchmarks
|
|
170
213
|
|
|
214
|
+
All benchmarks were run with Node.js 26.5.0 using the `tsx` TypeScript interpreter.
|
|
215
|
+
|
|
171
216
|
### Apple M3 Max · 36 GB RAM · macOS Sequoia 15.5
|
|
172
217
|
|
|
173
218
|
```
|
|
174
219
|
-----shallow state object-----
|
|
175
220
|
|
|
176
|
-
1,000 updates to "name" string val with 10 observers: 0.
|
|
177
|
-
1,000 updates to "name" string val with 100 observers: 0.
|
|
178
|
-
1,000 updates to "name" string val with 1000 observers: 0.
|
|
179
|
-
10,000 updates to "name" string val with 10 observers: 0.
|
|
180
|
-
10,000 updates to "name" string val with 100 observers: 0.
|
|
181
|
-
10,000 updates to "name" string val with 1000 observers: 0.
|
|
182
|
-
100,000 updates to "name" string val with 10 observers: 0.
|
|
183
|
-
100,000 updates to "name" string val with 100 observers: 0.
|
|
184
|
-
100,000 updates to "name" string val with 1000 observers: 0.
|
|
185
|
-
1,000,000 updates to "name" string val with 10 observers: 0.
|
|
186
|
-
1,000,000 updates to "name" string val with 100 observers: 0.
|
|
187
|
-
1,000,000 updates to "name" string val with 1000 observers:
|
|
221
|
+
1,000 updates to "name" string val with 10 observers: 0.0005343750000000114s
|
|
222
|
+
1,000 updates to "name" string val with 100 observers: 0.0006880840000000035s
|
|
223
|
+
1,000 updates to "name" string val with 1000 observers: 0.002814292000000023s
|
|
224
|
+
10,000 updates to "name" string val with 10 observers: 0.0007928750000000377s
|
|
225
|
+
10,000 updates to "name" string val with 100 observers: 0.0017347500000000195s
|
|
226
|
+
10,000 updates to "name" string val with 1000 observers: 0.007635916000000009s
|
|
227
|
+
100,000 updates to "name" string val with 10 observers: 0.01587337500000001s
|
|
228
|
+
100,000 updates to "name" string val with 100 observers: 0.020373334s
|
|
229
|
+
100,000 updates to "name" string val with 1000 observers: 0.12892133400000005s
|
|
230
|
+
1,000,000 updates to "name" string val with 10 observers: 0.14660462499999993s
|
|
231
|
+
1,000,000 updates to "name" string val with 100 observers: 0.20982341600000007s
|
|
232
|
+
1,000,000 updates to "name" string val with 1000 observers: 0.8595329590000002s
|
|
188
233
|
|
|
189
234
|
-----medium state object-----
|
|
190
235
|
|
|
191
|
-
1,000 updates to "user.settings.theme" string val with 10 observers: 0.
|
|
192
|
-
1,000 updates to "user.settings.theme" string val with 100 observers: 0.
|
|
193
|
-
1,000 updates to "user.settings.theme" string val with 1000 observers: 0.
|
|
194
|
-
10,000 updates to "user.settings.theme" string val with 10 observers: 0.
|
|
195
|
-
10,000 updates to "user.settings.theme" string val with 100 observers: 0.
|
|
196
|
-
10,000 updates to "user.settings.theme" string val with 1000 observers: 0.
|
|
197
|
-
100,000 updates to "user.settings.theme" string val with 10 observers: 0.
|
|
198
|
-
100,000 updates to "user.settings.theme" string val with 100 observers: 0.
|
|
199
|
-
100,000 updates to "user.settings.theme" string val with 1000 observers: 0.
|
|
200
|
-
1,000,000 updates to "user.settings.theme" string val with 10 observers: 0.
|
|
201
|
-
1,000,000 updates to "user.settings.theme" string val with 100 observers: 0.
|
|
202
|
-
1,000,000 updates to "user.settings.theme" string val with 1000 observers:
|
|
236
|
+
1,000 updates to "user.settings.theme" string val with 10 observers: 0.0011529580000001261s
|
|
237
|
+
1,000 updates to "user.settings.theme" string val with 100 observers: 0.002206040999999914s
|
|
238
|
+
1,000 updates to "user.settings.theme" string val with 1000 observers: 0.006846457999999984s
|
|
239
|
+
10,000 updates to "user.settings.theme" string val with 10 observers: 0.01919616700000006s
|
|
240
|
+
10,000 updates to "user.settings.theme" string val with 100 observers: 0.007707957999999963s
|
|
241
|
+
10,000 updates to "user.settings.theme" string val with 1000 observers: 0.014114458000000014s
|
|
242
|
+
100,000 updates to "user.settings.theme" string val with 10 observers: 0.012900958000000174s
|
|
243
|
+
100,000 updates to "user.settings.theme" string val with 100 observers: 0.018197167000000035s
|
|
244
|
+
100,000 updates to "user.settings.theme" string val with 1000 observers: 0.09116387499999973s
|
|
245
|
+
1,000,000 updates to "user.settings.theme" string val with 10 observers: 0.18448416599999973s
|
|
246
|
+
1,000,000 updates to "user.settings.theme" string val with 100 observers: 0.24980879200000028s
|
|
247
|
+
1,000,000 updates to "user.settings.theme" string val with 1000 observers: 0.8701055419999998s
|
|
203
248
|
|
|
204
249
|
-----deep state object-----
|
|
205
250
|
|
|
206
|
-
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.
|
|
207
|
-
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.
|
|
208
|
-
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.
|
|
209
|
-
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.
|
|
210
|
-
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.
|
|
211
|
-
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.
|
|
212
|
-
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.
|
|
213
|
-
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.
|
|
214
|
-
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.
|
|
215
|
-
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.
|
|
216
|
-
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.
|
|
217
|
-
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers:
|
|
251
|
+
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0002052080000003116s
|
|
252
|
+
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0002497920000000704s
|
|
253
|
+
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.001332209000000148s
|
|
254
|
+
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0018820000000000618s
|
|
255
|
+
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0024639170000000377s
|
|
256
|
+
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.014082249999999932s
|
|
257
|
+
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.021280209000000015s
|
|
258
|
+
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.02714695800000027s
|
|
259
|
+
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.09046704200000022s
|
|
260
|
+
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.2687363329999998s
|
|
261
|
+
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.3244868329999999s
|
|
262
|
+
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.9486893330000002s
|
|
218
263
|
```
|
|
219
264
|
|
|
220
265
|
### Intel i9-13900HX · 32 GB RAM · Windows 11
|
package/dist/react.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ import type { AllObjectKeys, PropType } from './types.js';
|
|
|
12
12
|
* The setter accepts either a new value directly or an updater function that
|
|
13
13
|
* receives the current value and returns the next one.
|
|
14
14
|
*
|
|
15
|
+
* The value is read through React's `useSyncExternalStore`, so the hook is
|
|
16
|
+
* safe to use during server-side rendering: the server renders the store's
|
|
17
|
+
* current value without subscribing, and the client subscribes during
|
|
18
|
+
* hydration.
|
|
19
|
+
*
|
|
15
20
|
* @example
|
|
16
21
|
* ```tsx
|
|
17
22
|
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
@@ -28,6 +33,37 @@ import type { AllObjectKeys, PropType } from './types.js';
|
|
|
28
33
|
* ```
|
|
29
34
|
*/
|
|
30
35
|
export declare function useTubular<T extends object, K extends string>(tubular: Tubular<T>, key: K & AllObjectKeys<T>): readonly [PropType<T, K> | null, (newValOrCallback: PropType<T, K> | ((prevVal: PropType<T, K> | null) => PropType<T, K>)) => void];
|
|
36
|
+
/**
|
|
37
|
+
* Creates a hook bound to a specific {@link Tubular} instance, so you never
|
|
38
|
+
* have to pass the instance around at every call site.
|
|
39
|
+
*
|
|
40
|
+
* Call this once, outside your components (the factory itself is not a hook,
|
|
41
|
+
* so module scope is safe), and it returns a hook that works exactly like
|
|
42
|
+
* {@link useTubular} minus the first argument — just the key path.
|
|
43
|
+
*
|
|
44
|
+
* The returned function is a hook: name it with a `use` prefix so React's
|
|
45
|
+
* rules-of-hooks linting recognizes it, and call it unconditionally at the top
|
|
46
|
+
* level of your components. Key paths remain fully type-checked against the
|
|
47
|
+
* bound store's state shape.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
52
|
+
*
|
|
53
|
+
* const useAppState = createUseTubular(store);
|
|
54
|
+
*
|
|
55
|
+
* function Counter() {
|
|
56
|
+
* const [count, setCount] = useAppState('count');
|
|
57
|
+
* return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* function Greeting() {
|
|
61
|
+
* const [name] = useAppState('user.name');
|
|
62
|
+
* return <p>Hello, {name}!</p>;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function createUseTubular<T extends object>(tubular: Tubular<T>): <K extends string>(key: K & AllObjectKeys<T>) => readonly [PropType<T, K & AllObjectKeys<T>> | null, (newValOrCallback: PropType<T, K & AllObjectKeys<T>> | ((prevVal: PropType<T, K & AllObjectKeys<T>> | null) => PropType<T, K & AllObjectKeys<T>>)) => void];
|
|
31
67
|
/**
|
|
32
68
|
* A React hook that returns a stable callback to reset a {@link Tubular} store
|
|
33
69
|
* back to its initial state.
|
|
@@ -51,3 +87,30 @@ export declare function useTubular<T extends object, K extends string>(tubular:
|
|
|
51
87
|
* ```
|
|
52
88
|
*/
|
|
53
89
|
export declare function useTubularReset<T extends object>(tubular: Tubular<T>): () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Creates a hook bound to a specific {@link Tubular} instance that returns a
|
|
92
|
+
* stable callback resetting that store back to its initial state.
|
|
93
|
+
*
|
|
94
|
+
* Call this once, outside your components (the factory itself is not a hook,
|
|
95
|
+
* so module scope is safe), and it returns a hook that works exactly like
|
|
96
|
+
* {@link useTubularReset} minus the first argument. Every bound
|
|
97
|
+
* {@link useTubular} (or {@link createUseTubular}) hook watching the store
|
|
98
|
+
* re-renders with the restored value.
|
|
99
|
+
*
|
|
100
|
+
* The returned function is a hook: name it with a `use` prefix so React's
|
|
101
|
+
* rules-of-hooks linting recognizes it, and call it unconditionally at the top
|
|
102
|
+
* level of your components.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* const store = new Tubular({ name: 'Alice', count: 0 });
|
|
107
|
+
*
|
|
108
|
+
* const useAppReset = createUseTubularReset(store);
|
|
109
|
+
*
|
|
110
|
+
* function ResetButton() {
|
|
111
|
+
* const handleReset = useAppReset();
|
|
112
|
+
* return <button onClick={handleReset}>Reset</button>;
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function createUseTubularReset<T extends object>(tubular: Tubular<T>): () => () => void;
|
package/dist/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* A React hook that binds to a {@link Tubular} instance and a specific key path
|
|
4
4
|
* within its state.
|
|
@@ -11,6 +11,11 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
11
11
|
* The setter accepts either a new value directly or an updater function that
|
|
12
12
|
* receives the current value and returns the next one.
|
|
13
13
|
*
|
|
14
|
+
* The value is read through React's `useSyncExternalStore`, so the hook is
|
|
15
|
+
* safe to use during server-side rendering: the server renders the store's
|
|
16
|
+
* current value without subscribing, and the client subscribes during
|
|
17
|
+
* hydration.
|
|
18
|
+
*
|
|
14
19
|
* @example
|
|
15
20
|
* ```tsx
|
|
16
21
|
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
@@ -27,9 +32,17 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
27
32
|
* ```
|
|
28
33
|
*/
|
|
29
34
|
export function useTubular(tubular, key) {
|
|
30
|
-
/** state */
|
|
31
|
-
const [val, setVal] = useState(() => tubular.read(key));
|
|
32
35
|
/** callbacks */
|
|
36
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
37
|
+
const observeCb = () => {
|
|
38
|
+
onStoreChange();
|
|
39
|
+
};
|
|
40
|
+
tubular.observe(key, observeCb);
|
|
41
|
+
return () => {
|
|
42
|
+
tubular.unobserve(key, observeCb);
|
|
43
|
+
};
|
|
44
|
+
}, [tubular, key]);
|
|
45
|
+
const getSnapshot = useCallback(() => tubular.read(key), [tubular, key]);
|
|
33
46
|
const handleUpdateVal = useCallback((newValOrCallback) => {
|
|
34
47
|
if (typeof newValOrCallback === 'function') {
|
|
35
48
|
tubular.update(key, newValOrCallback);
|
|
@@ -38,20 +51,43 @@ export function useTubular(tubular, key) {
|
|
|
38
51
|
tubular.update(key, () => newValOrCallback);
|
|
39
52
|
}
|
|
40
53
|
}, [tubular, key]);
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
// Sync in case the value changed between render and effect registration
|
|
44
|
-
setVal(tubular.read(key));
|
|
45
|
-
const observeCb = (newVal) => {
|
|
46
|
-
setVal(newVal);
|
|
47
|
-
};
|
|
48
|
-
tubular.observe(key, observeCb);
|
|
49
|
-
return () => {
|
|
50
|
-
tubular.unobserve(key, observeCb);
|
|
51
|
-
};
|
|
52
|
-
}, [tubular, key]);
|
|
54
|
+
/** state */
|
|
55
|
+
const val = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
53
56
|
return [val, handleUpdateVal];
|
|
54
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Creates a hook bound to a specific {@link Tubular} instance, so you never
|
|
60
|
+
* have to pass the instance around at every call site.
|
|
61
|
+
*
|
|
62
|
+
* Call this once, outside your components (the factory itself is not a hook,
|
|
63
|
+
* so module scope is safe), and it returns a hook that works exactly like
|
|
64
|
+
* {@link useTubular} minus the first argument — just the key path.
|
|
65
|
+
*
|
|
66
|
+
* The returned function is a hook: name it with a `use` prefix so React's
|
|
67
|
+
* rules-of-hooks linting recognizes it, and call it unconditionally at the top
|
|
68
|
+
* level of your components. Key paths remain fully type-checked against the
|
|
69
|
+
* bound store's state shape.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
74
|
+
*
|
|
75
|
+
* const useAppState = createUseTubular(store);
|
|
76
|
+
*
|
|
77
|
+
* function Counter() {
|
|
78
|
+
* const [count, setCount] = useAppState('count');
|
|
79
|
+
* return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* function Greeting() {
|
|
83
|
+
* const [name] = useAppState('user.name');
|
|
84
|
+
* return <p>Hello, {name}!</p>;
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function createUseTubular(tubular) {
|
|
89
|
+
return (key) => useTubular(tubular, key);
|
|
90
|
+
}
|
|
55
91
|
/**
|
|
56
92
|
* A React hook that returns a stable callback to reset a {@link Tubular} store
|
|
57
93
|
* back to its initial state.
|
|
@@ -77,3 +113,32 @@ export function useTubular(tubular, key) {
|
|
|
77
113
|
export function useTubularReset(tubular) {
|
|
78
114
|
return useCallback(() => tubular.reset(), [tubular]);
|
|
79
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Creates a hook bound to a specific {@link Tubular} instance that returns a
|
|
118
|
+
* stable callback resetting that store back to its initial state.
|
|
119
|
+
*
|
|
120
|
+
* Call this once, outside your components (the factory itself is not a hook,
|
|
121
|
+
* so module scope is safe), and it returns a hook that works exactly like
|
|
122
|
+
* {@link useTubularReset} minus the first argument. Every bound
|
|
123
|
+
* {@link useTubular} (or {@link createUseTubular}) hook watching the store
|
|
124
|
+
* re-renders with the restored value.
|
|
125
|
+
*
|
|
126
|
+
* The returned function is a hook: name it with a `use` prefix so React's
|
|
127
|
+
* rules-of-hooks linting recognizes it, and call it unconditionally at the top
|
|
128
|
+
* level of your components.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* const store = new Tubular({ name: 'Alice', count: 0 });
|
|
133
|
+
*
|
|
134
|
+
* const useAppReset = createUseTubularReset(store);
|
|
135
|
+
*
|
|
136
|
+
* function ResetButton() {
|
|
137
|
+
* const handleReset = useAppReset();
|
|
138
|
+
* return <button onClick={handleReset}>Reset</button>;
|
|
139
|
+
* }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export function createUseTubularReset(tubular) {
|
|
143
|
+
return () => useTubularReset(tubular);
|
|
144
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ type IsCollection<T> = T extends readonly any[] | Map<any, any> | Set<any> ? tru
|
|
|
8
8
|
/**
|
|
9
9
|
* Produces every dot-separated key path into T as a string union.
|
|
10
10
|
*
|
|
11
|
-
* - The `Depth` tuple acts as a recursion counter: when its length reaches
|
|
11
|
+
* - The `Depth` tuple acts as a recursion counter: when its length reaches 10 the
|
|
12
12
|
* type resolves to `never`, which caps the union size and prevents the language
|
|
13
13
|
* server from hanging on deeply-nested or self-referential shapes.
|
|
14
14
|
* - Top-level keys are included as bare strings (`K`), so this type supersedes
|
|
@@ -18,7 +18,7 @@ type IsCollection<T> = T extends readonly any[] | Map<any, any> | Set<any> ? tru
|
|
|
18
18
|
* - Returning `never` (not `''`) from every terminal branch avoids the need for
|
|
19
19
|
* a secondary `Extract<D, string>` pass over the full union.
|
|
20
20
|
*/
|
|
21
|
-
export type DotNestedKeys<T, Depth extends 1[] = []> = Depth['length'] extends
|
|
21
|
+
export type DotNestedKeys<T, Depth extends 1[] = []> = Depth['length'] extends 10 ? never : T extends object ? IsCollection<T> extends true ? never : {
|
|
22
22
|
[K in keyof T & string]: IsCollection<T[K]> extends true ? K : T[K] extends object ? K | `${K}.${DotNestedKeys<T[K], [...Depth, 1]>}` : K;
|
|
23
23
|
}[keyof T & string] : never;
|
|
24
24
|
/**
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A completely framework-agnostic, JavaScript / TypeScript state management and update broadcasting library wit zero dependencies, solid performance and a micro file size.",
|
|
4
4
|
"name": "totally-tubular",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"state",
|
|
9
9
|
"management",
|
|
@@ -26,27 +26,24 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"dist/"
|
|
28
28
|
],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "rm -rf dist/ && tsc --project ./tsconfig.build.json",
|
|
31
|
-
"clean": "rm -rf dist/",
|
|
32
|
-
"test": "bun test",
|
|
33
|
-
"test:watch": "bun test --watch"
|
|
34
|
-
},
|
|
35
29
|
"devDependencies": {
|
|
36
30
|
"@better-builds/lets-version": "^1.6.0",
|
|
37
31
|
"@biomejs/biome": "^2.4.16",
|
|
38
32
|
"@commitlint/cli": "^20.5.3",
|
|
39
33
|
"@commitlint/config-conventional": "^20.5.3",
|
|
40
|
-
"@happy-dom/global-registrator": "^20.10.2",
|
|
41
34
|
"@testing-library/react": "^16.3.2",
|
|
42
35
|
"@testing-library/user-event": "^14.6.1",
|
|
43
|
-
"@types/bun": "latest",
|
|
44
36
|
"@types/react": "^19.2.17",
|
|
37
|
+
"@types/node": "^26.1.1",
|
|
45
38
|
"@types/react-dom": "^19.2.3",
|
|
39
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
46
40
|
"happy-dom": "^20.10.2",
|
|
47
|
-
"react-dom": "^19.2.7",
|
|
48
41
|
"react": "^19.2.7",
|
|
49
|
-
"
|
|
42
|
+
"react-dom": "^19.2.7",
|
|
43
|
+
"tsx": "^4.23.0",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vite": "^8.1.4",
|
|
46
|
+
"vitest": "^4.1.10"
|
|
50
47
|
},
|
|
51
48
|
"peerDependencies": {
|
|
52
49
|
"react": ">=18"
|
|
@@ -74,5 +71,12 @@
|
|
|
74
71
|
"default": "./dist/types.js"
|
|
75
72
|
},
|
|
76
73
|
"./package.json": "./package.json"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "rm -rf dist/ && tsc --project ./tsconfig.build.json",
|
|
77
|
+
"clean": "rm -rf dist/",
|
|
78
|
+
"test": "vitest test",
|
|
79
|
+
"test:performance": "tsx ./performanceTests.ts",
|
|
80
|
+
"test:watch": "vitest test --watch"
|
|
77
81
|
}
|
|
78
|
-
}
|
|
82
|
+
}
|