totally-tubular 0.4.1 → 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 +43 -0
- package/dist/react.d.ts +63 -0
- package/dist/react.js +80 -15
- package/package.json +1 -1
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.**
|
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/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",
|