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 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, useEffect, useState } from 'react';
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
- /** effects */
42
- useEffect(() => {
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.4.1",
6
+ "version": "0.5.0",
7
7
  "keywords": [
8
8
  "state",
9
9
  "management",