react-hooks-global-states 15.0.9 → 15.0.12
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/GlobalStore.d.ts +39 -25
- package/GlobalStore.js +1 -1
- package/README.md +1438 -177
- package/README.proposal.md +1191 -0
- package/actions.d.ts +32 -0
- package/actions.js +1 -0
- package/bundle.js +1 -1
- package/createContext.d.ts +4 -549
- package/createContext.js +1 -1
- package/createGlobalState.d.ts +1 -191
- package/createGlobalState.js +1 -1
- package/index.d.ts +5 -4
- package/isRecord.js +1 -1
- package/package.json +10 -12
- package/shallowCompare.js +1 -1
- package/throwWrongKeyOnActionCollectionConfig.js +1 -1
- package/types.d.ts +878 -12
- package/uniqueId.d.ts +2 -71
- package/uniqueId.js +1 -1
- package/webpack.config.js +2 -16
package/README.md
CHANGED
|
@@ -2,357 +2,1618 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<div align="center">
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Zero setup. Zero complexity. Maximum performance.** 🚀
|
|
8
|
+
|
|
9
|
+
_One line of code. Infinite possibilities._ ✨
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
[](https://www.npmjs.com/package/react-hooks-global-states)
|
|
12
|
+
[](https://www.npmjs.com/package/react-hooks-global-states)
|
|
13
|
+
[](https://github.com/johnny-quesada-developer/react-hooks-global-states/blob/main/LICENSE)
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** specific for web applications (**local-storage integration**).
|
|
15
|
-
- **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** specific for React Native projects (**async-storage integration**).
|
|
15
|
+
[**Live Demo**](https://johnny-quesada-developer.github.io/global-hooks-example/) • [**Video Tutorial**](https://www.youtube.com/watch?v=1UBqXk2MH8I/) • [**CodePen**](https://codepen.io/johnnynabetes/pen/WNmeGwb?editors=0010)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
19
|
-
##
|
|
21
|
+
## 🎯 The One-Liner
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
```tsx
|
|
24
|
+
import { createGlobalState } from 'react-hooks-global-states';
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
export const useCounter = createGlobalState(0);
|
|
27
|
+
```
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
**That's it.** No providers. No context boilerplate. No configuration files. Just pure, beautiful state management. 🎨
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
```tsx
|
|
32
|
+
// Use it anywhere, instantly
|
|
33
|
+
function Counter() {
|
|
34
|
+
const [count, setCount] = useCounter();
|
|
35
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
31
38
|
|
|
32
39
|
---
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
-
|  |  |
|
|
37
|
-
| Quickly revert your application to a previous state. | Precisely debug specific actions affecting state changes. |
|
|
38
|
-
|
|
39
|
-
<br>
|
|
41
|
+
## 🚀 Why Developers Love This Library
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
<table>
|
|
44
|
+
<tr>
|
|
45
|
+
<td width="50%">
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
### 🎓 **Zero Learning Curve**
|
|
44
48
|
|
|
45
49
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// If you know this...
|
|
51
|
+
const [state, setState] = useState(0);
|
|
52
|
+
|
|
53
|
+
// You know this!
|
|
54
|
+
const [state, setState] = useGlobalState();
|
|
48
55
|
```
|
|
49
56
|
|
|
50
|
-
|
|
57
|
+
</td>
|
|
58
|
+
<td width="50%">
|
|
59
|
+
|
|
60
|
+
### ⚡ **Blazing Fast**
|
|
61
|
+
|
|
62
|
+
Only components that care about a slice re-render. Surgical precision, maximum performance.
|
|
51
63
|
|
|
52
64
|
```tsx
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
// Only re-renders when name changes
|
|
66
|
+
const [name] = useStore((s) => s.user.name);
|
|
55
67
|
```
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
<tr>
|
|
73
|
+
<td width="50%">
|
|
60
74
|
|
|
61
|
-
|
|
75
|
+
### 🔗 **Chainable Selectors**
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
```tsx
|
|
78
|
+
const useUsers = store.createSelectorHook((s) => s.users);
|
|
79
|
+
const useAdmins = useUsers.createSelectorHook((users) => users.filter((u) => u.isAdmin));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
</td>
|
|
83
|
+
<td width="50%">
|
|
84
|
+
|
|
85
|
+
### 🎭 **Actions (Optional)**
|
|
64
86
|
|
|
65
87
|
```tsx
|
|
66
|
-
|
|
88
|
+
const useAuth = createGlobalState(null, {
|
|
89
|
+
actions: {
|
|
90
|
+
login(credentials) {
|
|
91
|
+
return async ({ setState }) => {
|
|
92
|
+
const user = await api.login(credentials);
|
|
93
|
+
setState(user);
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
67
98
|
```
|
|
68
99
|
|
|
69
|
-
|
|
100
|
+
</td>
|
|
101
|
+
</tr>
|
|
102
|
+
|
|
103
|
+
<tr>
|
|
104
|
+
<td width="50%">
|
|
105
|
+
|
|
106
|
+
### 🎪 **Context Mode**
|
|
70
107
|
|
|
71
108
|
```tsx
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
))}
|
|
78
|
-
</ul>
|
|
79
|
-
);
|
|
109
|
+
const Form = createContext({ name: '', email: '' });
|
|
110
|
+
|
|
111
|
+
<Form.Provider>
|
|
112
|
+
<FormFields />
|
|
113
|
+
</Form.Provider>;
|
|
80
114
|
```
|
|
81
115
|
|
|
82
|
-
|
|
116
|
+
</td>
|
|
117
|
+
<td width="50%">
|
|
83
118
|
|
|
84
|
-
|
|
119
|
+
### � **Non-Reactive API**
|
|
120
|
+
|
|
121
|
+
Use state anywhere - even outside React components!
|
|
85
122
|
|
|
86
123
|
```tsx
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
124
|
+
// In API interceptors, WebSockets, utils...
|
|
125
|
+
const token = useAuth.getState().token;
|
|
126
|
+
useAuth.setState({ user: newUser });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
</table>
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 📦 Installation
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm install react-hooks-global-states
|
|
91
139
|
```
|
|
92
140
|
|
|
93
|
-
|
|
141
|
+
**Platform-specific with built-in storage:**
|
|
142
|
+
|
|
143
|
+
- 🌐 **Web**: `react-global-state-hooks` (localStorage)
|
|
144
|
+
- 📱 **React Native**: `react-native-global-state-hooks` (AsyncStorage by default, customizable, optional dependency)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 🎬 Quick Start
|
|
149
|
+
|
|
150
|
+
### 30 Seconds to Global State
|
|
94
151
|
|
|
95
152
|
```tsx
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
import { createGlobalState } from 'react-hooks-global-states';
|
|
154
|
+
|
|
155
|
+
// 1. Create it (anywhere)
|
|
156
|
+
const useTheme = createGlobalState('dark');
|
|
157
|
+
|
|
158
|
+
// 2. Use it (everywhere)
|
|
159
|
+
function ThemeToggle() {
|
|
160
|
+
const [theme, setTheme] = useTheme();
|
|
161
|
+
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme} mode</button>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function ThemedComponent() {
|
|
165
|
+
const [theme] = useTheme();
|
|
166
|
+
return <div className={theme}>Themed content</div>;
|
|
167
|
+
}
|
|
100
168
|
```
|
|
101
169
|
|
|
102
|
-
|
|
170
|
+
### 60 Seconds to Production-Ready
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { createGlobalState } from 'react-hooks-global-states';
|
|
174
|
+
|
|
175
|
+
const useAuth = createGlobalState(
|
|
176
|
+
{ user: null, token: null },
|
|
177
|
+
{
|
|
178
|
+
// Non-reactive metadata
|
|
179
|
+
metadata: { isLoading: false },
|
|
180
|
+
|
|
181
|
+
// Type-safe actions
|
|
182
|
+
actions: {
|
|
183
|
+
login(email, password) {
|
|
184
|
+
return async ({ setState, setMetadata }) => {
|
|
185
|
+
setMetadata({ isLoading: true });
|
|
186
|
+
try {
|
|
187
|
+
const { user, token } = await api.login(email, password);
|
|
188
|
+
setState({ user, token });
|
|
189
|
+
return { success: true };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return { success: false, error };
|
|
192
|
+
} finally {
|
|
193
|
+
setMetadata({ isLoading: false });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
logout() {
|
|
199
|
+
return ({ setState }) => {
|
|
200
|
+
setState({ user: null, token: null });
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Usage
|
|
208
|
+
function LoginForm() {
|
|
209
|
+
const [auth, actions] = useAuth();
|
|
210
|
+
const { isLoading } = useAuth.getMetadata();
|
|
211
|
+
|
|
212
|
+
const handleSubmit = async (e) => {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
const result = await actions.login(email, password);
|
|
215
|
+
if (!result.success) toast.error(result.error);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<form onSubmit={handleSubmit}>
|
|
220
|
+
{/* ... */}
|
|
221
|
+
<button disabled={isLoading}>{isLoading ? 'Logging in...' : 'Login'}</button>
|
|
222
|
+
</form>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
103
226
|
|
|
104
227
|
---
|
|
105
228
|
|
|
106
|
-
##
|
|
229
|
+
## 🌟 Core Features Deep Dive
|
|
230
|
+
|
|
231
|
+
### 1️⃣ Global State with `createGlobalState`
|
|
107
232
|
|
|
108
|
-
|
|
233
|
+
Create state that lives outside React's component tree. Perfect for app-wide state!
|
|
234
|
+
|
|
235
|
+
#### 🎨 The Basics
|
|
109
236
|
|
|
110
237
|
```tsx
|
|
111
|
-
|
|
112
|
-
|
|
238
|
+
// Primitives
|
|
239
|
+
const useCount = createGlobalState(0);
|
|
240
|
+
const useTheme = createGlobalState('light');
|
|
241
|
+
const useIsOpen = createGlobalState(false);
|
|
242
|
+
|
|
243
|
+
// Objects
|
|
244
|
+
const useUser = createGlobalState({ name: 'Guest', role: 'viewer' });
|
|
245
|
+
|
|
246
|
+
// Arrays
|
|
247
|
+
const useTodos = createGlobalState([
|
|
248
|
+
{ id: 1, text: 'Learn this library', done: true },
|
|
249
|
+
{ id: 2, text: 'Build something awesome', done: false },
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
// With function initialization (useful for testing - allows store.reset())
|
|
253
|
+
const useExpensiveState = createGlobalState(() => {
|
|
254
|
+
return computeExpensiveInitialValue();
|
|
255
|
+
});
|
|
113
256
|
```
|
|
114
257
|
|
|
115
|
-
|
|
258
|
+
#### 🎯 Surgical Re-renders with Selectors
|
|
259
|
+
|
|
260
|
+
The secret sauce: components only re-render when _their specific slice_ changes!
|
|
116
261
|
|
|
117
262
|
```tsx
|
|
118
|
-
const
|
|
119
|
-
|
|
263
|
+
const useStore = createGlobalState({
|
|
264
|
+
user: { name: 'John', age: 30, email: 'john@example.com' },
|
|
265
|
+
theme: 'dark',
|
|
266
|
+
notifications: [],
|
|
267
|
+
settings: { sound: true, vibrate: false },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// This component ONLY re-renders when user.name changes
|
|
271
|
+
function UserName() {
|
|
272
|
+
const [name] = useStore((state) => state.user.name);
|
|
273
|
+
return <h1>{name}</h1>;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Alternative: use.select() when you only need the value (not setState)
|
|
277
|
+
function UserNameAlt() {
|
|
278
|
+
const name = useStore.select((state) => state.user.name);
|
|
279
|
+
return <h1>{name}</h1>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// This ONLY re-renders when theme changes
|
|
283
|
+
function ThemeSwitcher() {
|
|
284
|
+
const [theme, setStore] = useStore((state) => state.theme);
|
|
285
|
+
|
|
286
|
+
const toggleTheme = () => {
|
|
287
|
+
setStore((s) => ({ ...s, theme: theme === 'dark' ? 'light' : 'dark' }));
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return <button onClick={toggleTheme}>{theme}</button>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// This ONLY re-renders when notifications array changes
|
|
294
|
+
function NotificationCount() {
|
|
295
|
+
const [notifications] = useStore((state) => state.notifications);
|
|
296
|
+
return <span>{notifications.length}</span>;
|
|
297
|
+
}
|
|
120
298
|
```
|
|
121
299
|
|
|
122
|
-
|
|
300
|
+
**Performance comparison:**
|
|
123
301
|
|
|
124
|
-
|
|
302
|
+
| Approach | Re-renders when ANY state changes? |
|
|
303
|
+
| --------------- | ---------------------------------- |
|
|
304
|
+
| Context (naive) | ✅ YES (performance killer) |
|
|
305
|
+
| This library | ❌ NO (only selected slices) |
|
|
306
|
+
|
|
307
|
+
#### ⚡ Computed Values with Dependencies
|
|
308
|
+
|
|
309
|
+
Derive values efficiently with automatic recomputation control! - selectors can depend on external state (like `useState`)!
|
|
125
310
|
|
|
126
311
|
```tsx
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
312
|
+
const useStore = createGlobalState({
|
|
313
|
+
todos: [
|
|
314
|
+
{ id: 1, text: 'Task 1', completed: false, priority: 'high' },
|
|
315
|
+
{ id: 2, text: 'Task 2', completed: true, priority: 'low' },
|
|
316
|
+
{ id: 3, text: 'Task 3', completed: false, priority: 'high' },
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
function FilteredTodoList() {
|
|
321
|
+
// These are regular useState - NOT in the global store!
|
|
322
|
+
const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
|
|
323
|
+
const [priorityFilter, setPriorityFilter] = useState('all'); // 'all' | 'high' | 'low'
|
|
324
|
+
|
|
325
|
+
// 🔥 The magic: selector recomputes when EITHER store OR external state changes!
|
|
326
|
+
const [filteredTodos] = useStore(
|
|
327
|
+
(state) => {
|
|
328
|
+
let todos = state.todos;
|
|
329
|
+
|
|
330
|
+
// Filter by completion
|
|
331
|
+
if (filter === 'active') todos = todos.filter((t) => !t.completed);
|
|
332
|
+
if (filter === 'completed') todos = todos.filter((t) => t.completed);
|
|
333
|
+
|
|
334
|
+
// Filter by priority
|
|
335
|
+
if (priorityFilter !== 'all') {
|
|
336
|
+
todos = todos.filter((t) => t.priority === priorityFilter);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return todos;
|
|
340
|
+
},
|
|
341
|
+
[filter, priorityFilter], // Recompute when these external dependencies change!
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div>
|
|
346
|
+
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
|
|
347
|
+
<option value="all">All</option>
|
|
348
|
+
<option value="active">Active</option>
|
|
349
|
+
<option value="completed">Completed</option>
|
|
350
|
+
</select>
|
|
351
|
+
|
|
352
|
+
<select value={priorityFilter} onChange={(e) => setPriorityFilter(e.target.value)}>
|
|
353
|
+
<option value="all">All Priorities</option>
|
|
354
|
+
<option value="high">High</option>
|
|
355
|
+
<option value="low">Low</option>
|
|
356
|
+
</select>
|
|
357
|
+
|
|
358
|
+
<ul>
|
|
359
|
+
{filteredTodos.map((todo) => (
|
|
360
|
+
<li key={todo.id}>{todo.text}</li>
|
|
361
|
+
))}
|
|
362
|
+
</ul>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Advanced: use config object with custom equality
|
|
368
|
+
function AdvancedFiltered() {
|
|
369
|
+
const [filter, setFilter] = useState('all');
|
|
370
|
+
|
|
371
|
+
const [filteredTodos] = useStore(
|
|
372
|
+
(state) =>
|
|
373
|
+
state.todos.filter((t) => (filter === 'all' ? true : t.completed === (filter === 'completed'))),
|
|
374
|
+
{
|
|
375
|
+
dependencies: [filter],
|
|
376
|
+
// Custom equality - only recompute if todos array changed
|
|
377
|
+
isEqualRoot: (prev, next) => prev.todos === next.todos,
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return <ul>{/* ... */}</ul>;
|
|
382
|
+
}
|
|
131
383
|
```
|
|
132
384
|
|
|
133
|
-
####
|
|
385
|
+
#### 🔗 Reusable Selector Hooks (The Game Changer!)
|
|
134
386
|
|
|
135
|
-
|
|
387
|
+
Create hooks from hooks! Chain them! Compose them! This is where it gets fun! 🎉
|
|
136
388
|
|
|
137
389
|
```tsx
|
|
138
|
-
const
|
|
139
|
-
|
|
390
|
+
const useStore = createGlobalState({
|
|
391
|
+
users: [
|
|
392
|
+
{ id: 1, name: 'Alice', role: 'admin', active: true },
|
|
393
|
+
{ id: 2, name: 'Bob', role: 'user', active: true },
|
|
394
|
+
{ id: 3, name: 'Charlie', role: 'admin', active: false },
|
|
395
|
+
],
|
|
396
|
+
currentUserId: 1,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Create a reusable hook for users array
|
|
400
|
+
const useUsers = useStore.createSelectorHook((state) => state.users);
|
|
401
|
+
|
|
402
|
+
// Chain it! Create a hook for active users only
|
|
403
|
+
const useActiveUsers = useUsers.createSelectorHook((users) => users.filter((u) => u.active));
|
|
140
404
|
|
|
141
|
-
|
|
405
|
+
// Chain it again! Create a hook for active admins
|
|
406
|
+
const useActiveAdmins = useActiveUsers.createSelectorHook((users) => users.filter((u) => u.role === 'admin'));
|
|
407
|
+
|
|
408
|
+
// Create a hook for the current user
|
|
409
|
+
const useCurrentUser = useStore.createSelectorHook((state) => {
|
|
410
|
+
return state.users.find((u) => u.id === state.currentUserId);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Now use them anywhere!
|
|
414
|
+
function UserStats() {
|
|
415
|
+
const [totalUsers] = useUsers();
|
|
416
|
+
const [activeUsers] = useActiveUsers();
|
|
417
|
+
const [activeAdmins] = useActiveAdmins();
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<div>
|
|
421
|
+
<p>Total: {totalUsers.length}</p>
|
|
422
|
+
<p>Active: {activeUsers.length}</p>
|
|
423
|
+
<p>Active Admins: {activeAdmins.length}</p>
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function CurrentUserProfile() {
|
|
429
|
+
const [user] = useCurrentUser();
|
|
430
|
+
return <div>{user?.name}</div>;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 🎯 Key insight: All these hooks share the SAME setState!
|
|
434
|
+
function AnyComponent() {
|
|
435
|
+
const [, setFromUsers] = useUsers();
|
|
436
|
+
const [, setFromAdmins] = useActiveAdmins();
|
|
437
|
+
const [, setFromStore] = useStore();
|
|
438
|
+
|
|
439
|
+
console.log(setFromUsers === setFromAdmins); // true!
|
|
440
|
+
console.log(setFromUsers === setFromStore); // true!
|
|
441
|
+
console.log(setFromUsers === useStore.setState); // true!
|
|
442
|
+
}
|
|
142
443
|
```
|
|
143
444
|
|
|
144
|
-
|
|
445
|
+
**Why this is powerful:**
|
|
145
446
|
|
|
146
|
-
|
|
447
|
+
| Feature | Benefit |
|
|
448
|
+
| -------------------- | ------------------------------------------ |
|
|
449
|
+
| 🎯 **Precision** | Each hook subscribes to only what it needs |
|
|
450
|
+
| 🔗 **Composability** | Build complex selectors from simple ones |
|
|
451
|
+
| ♻️ **Reusability** | Define once, use everywhere |
|
|
452
|
+
| 🎨 **Clean Code** | No repetitive selector logic in components |
|
|
453
|
+
| ⚡ **Performance** | Automatic memoization and change detection |
|
|
147
454
|
|
|
148
|
-
|
|
455
|
+
#### 🎬 Actions - When You Need Structure
|
|
456
|
+
|
|
457
|
+
Actions aren't required, but they're awesome for organizing mutations!
|
|
149
458
|
|
|
150
459
|
```tsx
|
|
151
|
-
|
|
152
|
-
{
|
|
460
|
+
const useStore = createGlobalState(
|
|
461
|
+
{
|
|
462
|
+
todos: new Map(), // Map<id, todo>
|
|
463
|
+
filter: 'all',
|
|
464
|
+
},
|
|
153
465
|
{
|
|
154
466
|
actions: {
|
|
155
|
-
|
|
156
|
-
return
|
|
157
|
-
const
|
|
158
|
-
|
|
467
|
+
addTodo(text) {
|
|
468
|
+
return ({ setState, getState }) => {
|
|
469
|
+
const id = Date.now();
|
|
470
|
+
const newTodo = { id, text, completed: false };
|
|
471
|
+
|
|
472
|
+
setState((s) => ({
|
|
473
|
+
...s,
|
|
474
|
+
todos: new Map(s.todos).set(id, newTodo),
|
|
475
|
+
}));
|
|
159
476
|
};
|
|
160
477
|
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
478
|
+
|
|
479
|
+
toggleTodo(id) {
|
|
480
|
+
return ({ setState, getState }) => {
|
|
481
|
+
const { todos } = getState();
|
|
482
|
+
const todo = todos.get(id);
|
|
483
|
+
if (!todo) return;
|
|
484
|
+
|
|
485
|
+
setState((s) => ({
|
|
486
|
+
...s,
|
|
487
|
+
todos: new Map(s.todos).set(id, { ...todo, completed: !todo.completed }),
|
|
488
|
+
}));
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
clearCompleted() {
|
|
493
|
+
return ({ setState, getState }) => {
|
|
494
|
+
const { todos } = getState();
|
|
495
|
+
|
|
496
|
+
// Filter out completed todos, create new Map
|
|
497
|
+
const newTodos = new Map();
|
|
498
|
+
todos.forEach((todo, id) => {
|
|
499
|
+
if (!todo.completed) newTodos.set(id, todo);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
setState((s) => ({ ...s, todos: newTodos }));
|
|
503
|
+
|
|
504
|
+
// Actions can call other actions!
|
|
505
|
+
this.updateStats();
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
updateStats() {
|
|
510
|
+
return ({ getState }) => {
|
|
511
|
+
const { todos } = getState();
|
|
512
|
+
const completed = Array.from(todos.values()).filter((t) => t.completed).length;
|
|
513
|
+
console.log(`${completed} completed`);
|
|
514
|
+
};
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// Async actions? No problem!
|
|
518
|
+
syncWithServer() {
|
|
519
|
+
return async ({ setState }) => {
|
|
520
|
+
const todosArray = await api.fetchTodos();
|
|
521
|
+
const todosMap = new Map(todosArray.map((t) => [t.id, t]));
|
|
522
|
+
setState((s) => ({ ...s, todos: todosMap }));
|
|
164
523
|
};
|
|
165
524
|
},
|
|
166
525
|
},
|
|
167
526
|
},
|
|
168
527
|
);
|
|
528
|
+
|
|
529
|
+
// When actions are defined, the second element is the actions object!
|
|
530
|
+
function TodoApp() {
|
|
531
|
+
const [state, actions] = useStore();
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<div>
|
|
535
|
+
<button onClick={() => actions.addTodo('New task')}>Add</button>
|
|
536
|
+
<button onClick={() => actions.clearCompleted()}>Clear</button>
|
|
537
|
+
<button onClick={() => actions.syncWithServer()}>Sync</button>
|
|
538
|
+
|
|
539
|
+
{Array.from(state.todos.values()).map((todo) => (
|
|
540
|
+
<div key={todo.id} onClick={() => actions.toggleTodo(todo.id)}>
|
|
541
|
+
{todo.text}
|
|
542
|
+
</div>
|
|
543
|
+
))}
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// � Don't worry, you can still go rogue with setState!
|
|
549
|
+
const handleQuickFix = () => {
|
|
550
|
+
useStore.setState((s) => ({ ...s, filter: 'all' }));
|
|
551
|
+
};
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
#### 🔌 Non-Reactive API (Use Outside React!)
|
|
555
|
+
|
|
556
|
+
Access your state from _anywhere_ - even outside components!
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
const useAuth = createGlobalState({ user: null, token: null });
|
|
560
|
+
|
|
561
|
+
// ✨ In a utility file
|
|
562
|
+
export async function loginUser(email, password) {
|
|
563
|
+
try {
|
|
564
|
+
const { user, token } = await api.login(email, password);
|
|
565
|
+
useAuth.setState({ user, token });
|
|
566
|
+
return { success: true };
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return { success: false, error };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ✨ In an API interceptor
|
|
573
|
+
axios.interceptors.request.use((config) => {
|
|
574
|
+
const { token } = useAuth.getState();
|
|
575
|
+
if (token) {
|
|
576
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
577
|
+
}
|
|
578
|
+
return config;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ✨ In a WebSocket handler
|
|
582
|
+
socket.on('user-updated', (user) => {
|
|
583
|
+
useAuth.setState((state) => ({ ...state, user }));
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ✨ Subscribe to changes (even outside React!)
|
|
587
|
+
const unsubscribe = useAuth.subscribe(
|
|
588
|
+
// Selector
|
|
589
|
+
(state) => state.user,
|
|
590
|
+
// Callback
|
|
591
|
+
function onChange(newUser) {
|
|
592
|
+
console.log('User changed:', user);
|
|
593
|
+
analytics.identify(user?.id);
|
|
594
|
+
},
|
|
595
|
+
);
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### 🔭 Observable Fragments (RxJS-style)
|
|
599
|
+
|
|
600
|
+
Create observable slices of your state for reactive programming!
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
const useStore = createGlobalState({
|
|
604
|
+
count: 0,
|
|
605
|
+
user: { name: 'John' },
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Create an observable that tracks just the count
|
|
609
|
+
const countObservable = useStore.createObservable((state) => state.count);
|
|
610
|
+
|
|
611
|
+
// Use it outside React
|
|
612
|
+
countObservable.subscribe((count) => {
|
|
613
|
+
console.log('Count is now:', count);
|
|
614
|
+
if (count > 10) {
|
|
615
|
+
alert('Count is high!');
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Observables also have getState
|
|
620
|
+
console.log(countObservable.getState()); // Current count value
|
|
621
|
+
|
|
622
|
+
// Chain observables!
|
|
623
|
+
const doubledObservable = countObservable.createObservable((count) => count * 2);
|
|
169
624
|
```
|
|
170
625
|
|
|
171
|
-
|
|
626
|
+
#### 📋 Metadata - Non-Reactive Side Info
|
|
627
|
+
|
|
628
|
+
Store data that doesn't need to trigger re-renders!
|
|
172
629
|
|
|
173
630
|
```tsx
|
|
174
|
-
const
|
|
631
|
+
const useStore = createGlobalState(
|
|
632
|
+
{ items: [] },
|
|
633
|
+
{
|
|
634
|
+
metadata: {
|
|
635
|
+
isLoading: false,
|
|
636
|
+
lastFetch: null,
|
|
637
|
+
error: null,
|
|
638
|
+
retryCount: 0,
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// Metadata changes don't trigger re-renders!
|
|
644
|
+
useStore.setMetadata({ isLoading: true });
|
|
645
|
+
|
|
646
|
+
// But you can access it anytime
|
|
647
|
+
const meta = useStore.getMetadata();
|
|
648
|
+
console.log(meta.isLoading); // true
|
|
649
|
+
|
|
650
|
+
// Perfect for loading states, error tracking, etc.
|
|
651
|
+
async function fetchData() {
|
|
652
|
+
useStore.setMetadata({ isLoading: true, error: null });
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const items = await api.fetch();
|
|
656
|
+
useStore.setState({ items });
|
|
657
|
+
useStore.setMetadata({
|
|
658
|
+
isLoading: false,
|
|
659
|
+
lastFetch: new Date(),
|
|
660
|
+
});
|
|
661
|
+
} catch (error) {
|
|
662
|
+
useStore.setMetadata({
|
|
663
|
+
isLoading: false,
|
|
664
|
+
error: error.message,
|
|
665
|
+
retryCount: useStore.getMetadata().retryCount + 1,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
175
669
|
```
|
|
176
670
|
|
|
177
671
|
---
|
|
178
672
|
|
|
179
|
-
|
|
673
|
+
### 2️⃣ Scoped State with `createContext`
|
|
180
674
|
|
|
181
|
-
|
|
182
|
-
|
|
675
|
+
Sometimes you need state scoped to a component tree. That's what `createContext` is for!
|
|
676
|
+
|
|
677
|
+
#### 🎪 The Basics
|
|
183
678
|
|
|
184
679
|
```tsx
|
|
185
|
-
|
|
680
|
+
import { createContext } from 'react-hooks-global-states';
|
|
681
|
+
|
|
682
|
+
// Create a context
|
|
683
|
+
const UserFormContext = createContext({
|
|
684
|
+
name: '',
|
|
685
|
+
email: '',
|
|
686
|
+
age: 0,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
function App() {
|
|
690
|
+
return (
|
|
691
|
+
<UserFormContext.Provider>
|
|
692
|
+
<FormFields />
|
|
693
|
+
<FormPreview />
|
|
694
|
+
</UserFormContext.Provider>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function FormFields() {
|
|
699
|
+
const [form, setForm] = UserFormContext.use();
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<div>
|
|
703
|
+
<input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
|
|
704
|
+
<input value={form.email} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
|
|
705
|
+
</div>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function FormPreview() {
|
|
710
|
+
const [form] = UserFormContext.use();
|
|
711
|
+
return (
|
|
712
|
+
<div>
|
|
713
|
+
Hello {form.name} ({form.email})
|
|
714
|
+
</div>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
186
717
|
```
|
|
187
718
|
|
|
188
|
-
####
|
|
719
|
+
#### 🎁 Provider Variations
|
|
189
720
|
|
|
190
721
|
```tsx
|
|
191
|
-
const
|
|
192
|
-
|
|
722
|
+
const ThemeContext = createContext('light');
|
|
723
|
+
|
|
724
|
+
function App() {
|
|
725
|
+
return (
|
|
726
|
+
<>
|
|
727
|
+
{/* Default value */}
|
|
728
|
+
<ThemeContext.Provider>
|
|
729
|
+
<Page /> {/* Gets 'light' */}
|
|
730
|
+
</ThemeContext.Provider>
|
|
731
|
+
|
|
732
|
+
{/* Custom value */}
|
|
733
|
+
<ThemeContext.Provider value="dark">
|
|
734
|
+
<Page /> {/* Gets 'dark' */}
|
|
735
|
+
</ThemeContext.Provider>
|
|
736
|
+
|
|
737
|
+
{/* Derived from parent (yes, you can nest!) */}
|
|
738
|
+
<ThemeContext.Provider value={(parent) => (parent === 'dark' ? 'light' : 'dark')}>
|
|
739
|
+
<Page /> {/* Gets opposite of parent */}
|
|
740
|
+
</ThemeContext.Provider>
|
|
741
|
+
</>
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
#### 🎯 Context + Selectors = ❤️
|
|
747
|
+
|
|
748
|
+
Everything that works with `createGlobalState` works with `createContext`!
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
const FormContext = createContext({
|
|
752
|
+
personal: { name: '', age: 0 },
|
|
753
|
+
contact: { email: '', phone: '' },
|
|
754
|
+
preferences: { theme: 'light', notifications: true },
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Only re-renders when name changes!
|
|
758
|
+
function NameField() {
|
|
759
|
+
const [name, setForm] = FormContext.use((state) => state.personal.name);
|
|
760
|
+
return (
|
|
761
|
+
<input
|
|
762
|
+
value={name}
|
|
763
|
+
onChange={(e) => setForm((s) => ({ ...s, personal: { ...s.personal, name: e.target.value } }))}
|
|
764
|
+
/>
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Only re-renders when email changes!
|
|
769
|
+
function EmailField() {
|
|
770
|
+
const [email] = FormContext.use((state) => state.contact.email);
|
|
771
|
+
return <div>{email}</div>;
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
#### 🎭 Context with Actions
|
|
776
|
+
|
|
777
|
+
```tsx
|
|
778
|
+
const CounterContext = createContext(0, {
|
|
779
|
+
actions: {
|
|
780
|
+
increment(amount = 1) {
|
|
781
|
+
return ({ setState, getState }) => {
|
|
782
|
+
setState(getState() + amount);
|
|
783
|
+
};
|
|
784
|
+
},
|
|
785
|
+
decrement(amount = 1) {
|
|
786
|
+
return ({ setState, getState }) => {
|
|
787
|
+
setState(getState() - amount);
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
reset() {
|
|
791
|
+
return ({ setState }) => setState(0);
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
function Counter() {
|
|
797
|
+
const [count, actions] = CounterContext.use();
|
|
798
|
+
|
|
799
|
+
return (
|
|
800
|
+
<div>
|
|
801
|
+
<h1>{count}</h1>
|
|
802
|
+
<button onClick={() => actions.increment()}>+</button>
|
|
803
|
+
<button onClick={() => actions.decrement()}>-</button>
|
|
804
|
+
<button onClick={() => actions.reset()}>Reset</button>
|
|
805
|
+
</div>
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function App() {
|
|
810
|
+
return (
|
|
811
|
+
<CounterContext.Provider>
|
|
812
|
+
<Counter />
|
|
813
|
+
</CounterContext.Provider>
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
#### 🔗 Reusable Context Selectors
|
|
819
|
+
|
|
820
|
+
Yes, chainable selectors work here too!
|
|
821
|
+
|
|
822
|
+
```tsx
|
|
823
|
+
const DataContext = createContext({
|
|
824
|
+
users: [
|
|
825
|
+
/* ... */
|
|
826
|
+
],
|
|
827
|
+
posts: [
|
|
828
|
+
/* ... */
|
|
829
|
+
],
|
|
830
|
+
filter: 'all',
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Create reusable hooks
|
|
834
|
+
const useUsers = DataContext.use.createSelectorHook((s) => s.users);
|
|
835
|
+
const useActiveUsers = useUsers.createSelectorHook((u) => u.filter((user) => user.active));
|
|
836
|
+
const usePosts = DataContext.use.createSelectorHook((s) => s.posts);
|
|
837
|
+
|
|
838
|
+
function App() {
|
|
839
|
+
return (
|
|
840
|
+
<DataContext.Provider>
|
|
841
|
+
<UserList />
|
|
842
|
+
<PostList />
|
|
843
|
+
</DataContext.Provider>
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function UserList() {
|
|
848
|
+
const [activeUsers] = useActiveUsers();
|
|
849
|
+
return (
|
|
850
|
+
<ul>
|
|
851
|
+
{activeUsers.map((u) => (
|
|
852
|
+
<li key={u.id}>{u.name}</li>
|
|
853
|
+
))}
|
|
854
|
+
</ul>
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
#### 🧪 What About Testing?
|
|
860
|
+
|
|
861
|
+
Do you need to access the context to spy on state changes, inject test data, or manipulate state directly? No problem!
|
|
862
|
+
|
|
863
|
+
```tsx
|
|
864
|
+
import { renderHook } from '@testing-library/react';
|
|
865
|
+
import CounterContext from './CounterContext'; // { count: 0 } with increment() action
|
|
866
|
+
|
|
867
|
+
describe('CounterContext', () => {
|
|
868
|
+
it('should manipulate state from tests', () => {
|
|
869
|
+
// Create wrapper with direct access to context
|
|
870
|
+
const { wrapper, context } = CounterContext.Provider.makeProviderWrapper();
|
|
871
|
+
|
|
872
|
+
// Render the hook
|
|
873
|
+
const { result } = renderHook(() => CounterContext.use(), { wrapper });
|
|
874
|
+
|
|
875
|
+
// Read initial state
|
|
876
|
+
expect(result.current[0].count).toBe(0);
|
|
877
|
+
|
|
878
|
+
// Call the action through the wrapper
|
|
879
|
+
context.current.actions.increment();
|
|
880
|
+
|
|
881
|
+
// Verify state updated
|
|
882
|
+
expect(result.current[0].count).toBe(1);
|
|
883
|
+
});
|
|
193
884
|
});
|
|
194
885
|
```
|
|
195
886
|
|
|
196
|
-
####
|
|
887
|
+
#### 🎬 Lifecycle Hooks
|
|
888
|
+
|
|
889
|
+
React to context lifecycle events!
|
|
197
890
|
|
|
198
891
|
```tsx
|
|
199
|
-
const
|
|
892
|
+
const DataContext = createContext([], {
|
|
200
893
|
callbacks: {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
894
|
+
onCreated: (store) => {
|
|
895
|
+
console.log('Context created with:', store.getState());
|
|
896
|
+
},
|
|
897
|
+
onMounted: (store) => {
|
|
898
|
+
console.log('Provider mounted!');
|
|
899
|
+
|
|
900
|
+
// Fetch data on mount
|
|
901
|
+
fetchData().then((data) => store.setState(data));
|
|
902
|
+
|
|
903
|
+
// Return cleanup
|
|
904
|
+
return () => {
|
|
905
|
+
console.log('Provider unmounting!');
|
|
906
|
+
};
|
|
208
907
|
},
|
|
209
908
|
},
|
|
210
909
|
});
|
|
910
|
+
|
|
911
|
+
// Or per-provider
|
|
912
|
+
function App() {
|
|
913
|
+
return (
|
|
914
|
+
<DataContext.Provider
|
|
915
|
+
onCreated={(store) => console.log('Created!')}
|
|
916
|
+
onMounted={(store) => {
|
|
917
|
+
console.log('Mounted!');
|
|
918
|
+
return () => console.log('Cleanup!');
|
|
919
|
+
}}
|
|
920
|
+
>
|
|
921
|
+
<Content />
|
|
922
|
+
</DataContext.Provider>
|
|
923
|
+
);
|
|
924
|
+
}
|
|
211
925
|
```
|
|
212
926
|
|
|
213
927
|
---
|
|
214
928
|
|
|
215
|
-
|
|
929
|
+
### 3️⃣ External Actions with `actions`
|
|
930
|
+
|
|
931
|
+
Extend any store with additional actions without modifying it! Perfect for separating concerns! 🎯
|
|
932
|
+
|
|
933
|
+
#### 💪 Direct Binding
|
|
934
|
+
|
|
935
|
+
```tsx
|
|
936
|
+
import { createGlobalState, actions } from 'react-hooks-global-states';
|
|
937
|
+
|
|
938
|
+
const useCounter = createGlobalState(0);
|
|
939
|
+
|
|
940
|
+
// Create actions for the store
|
|
941
|
+
const counterActions = actions(useCounter, {
|
|
942
|
+
increment(amount = 1) {
|
|
943
|
+
return ({ setState, getState }) => {
|
|
944
|
+
setState(getState() + amount);
|
|
945
|
+
};
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
decrement(amount = 1) {
|
|
949
|
+
return ({ setState, getState }) => {
|
|
950
|
+
setState(getState() - amount);
|
|
951
|
+
};
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
double() {
|
|
955
|
+
return ({ setState, getState }) => {
|
|
956
|
+
setState(getState() * 2);
|
|
957
|
+
};
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Use them anywhere!
|
|
962
|
+
counterActions.increment(5); // count = 5
|
|
963
|
+
counterActions.double(); // count = 10
|
|
964
|
+
counterActions.decrement(3); // count = 7
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
#### 🎨 Action Templates (Define Before Ready!)
|
|
968
|
+
|
|
969
|
+
Need actions in contexts or lifecycle hooks (onInit) before the store API is available? Create action templates first, bind them later!
|
|
970
|
+
|
|
971
|
+
```tsx
|
|
972
|
+
import { actions, InferAPI, createContext } from 'react-hooks-global-states';
|
|
973
|
+
|
|
974
|
+
type SessionAPI = InferAPI<typeof SessionContext>;
|
|
975
|
+
|
|
976
|
+
// Create internal actions template
|
|
977
|
+
const internalActions = actions<SessionAPI>()({
|
|
978
|
+
loadData() {
|
|
979
|
+
return async ({ setState }) => {
|
|
980
|
+
...
|
|
981
|
+
};
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
const SessionContext = createContext(
|
|
986
|
+
{ user: null, preferences: {}, lastSync: null },
|
|
987
|
+
{
|
|
988
|
+
callbacks: {
|
|
989
|
+
onInit: (api) => {
|
|
990
|
+
const { loadData } = internalActions(api);
|
|
991
|
+
|
|
992
|
+
// Load data on init
|
|
993
|
+
loadData();
|
|
994
|
+
|
|
995
|
+
// Sync every 5 minutes
|
|
996
|
+
const interval = setInterval(loadData, 5 * 60 * 1000);
|
|
997
|
+
|
|
998
|
+
// Cleanup on unmount
|
|
999
|
+
return () => clearInterval(interval);
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
// public actions
|
|
1004
|
+
actions: {
|
|
1005
|
+
logout() {
|
|
1006
|
+
return (api) => {
|
|
1007
|
+
const { setState } = api as SessionAPI;
|
|
1008
|
+
setState({ user: null, preferences: {}, lastSync: null });
|
|
1009
|
+
};
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
// Internal actions are not expose through the context
|
|
1016
|
+
const { loadData } = SessionContext.use.actions();
|
|
1017
|
+
console.log(loadData); // undefined
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
#### 🔄 Actions Calling Actions
|
|
1021
|
+
|
|
1022
|
+
Actions can call each other in multiple ways!
|
|
1023
|
+
|
|
1024
|
+
```tsx
|
|
1025
|
+
const useStore = createGlobalState({ count: 0, history: [] }, {
|
|
1026
|
+
actions: {
|
|
1027
|
+
clearHistory() {
|
|
1028
|
+
return ({ setState }) => {
|
|
1029
|
+
...
|
|
1030
|
+
};
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const storeActions = actions(useStore, {
|
|
1036
|
+
logAction(message) {
|
|
1037
|
+
return ({ setState, getState }) => {
|
|
1038
|
+
...
|
|
1039
|
+
};
|
|
1040
|
+
},
|
|
1041
|
+
|
|
1042
|
+
increment(amount = 1) {
|
|
1043
|
+
return ({ setState, getState }) => {
|
|
1044
|
+
setState((s) => ({ ...s, count: s.count + amount }));
|
|
1045
|
+
|
|
1046
|
+
// Call another action in this group with 'this'
|
|
1047
|
+
this.logAction(`Incremented by ${amount}`);
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
incrementTwice(amount = 1) {
|
|
1052
|
+
return ({ actions }) => {
|
|
1053
|
+
// Call actions with 'this'
|
|
1054
|
+
this.increment(amount);
|
|
1055
|
+
this.increment(amount);
|
|
1056
|
+
|
|
1057
|
+
// Or directly from storeActions
|
|
1058
|
+
storeActions.logAction(`Incremented twice by ${amount}`);
|
|
1059
|
+
|
|
1060
|
+
// Access store's public actions via 'actions' parameter
|
|
1061
|
+
actions.clearHistory();
|
|
1062
|
+
};
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
storeActions.incrementTwice(5);
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
#### 🎭 Access Store Actions
|
|
1070
|
+
|
|
1071
|
+
External actions can call each other with `this` and access the store's public actions!
|
|
1072
|
+
|
|
1073
|
+
```tsx
|
|
1074
|
+
const useStore = createGlobalState(
|
|
1075
|
+
{ count: 0, logs: [] },
|
|
1076
|
+
{
|
|
1077
|
+
actions: {
|
|
1078
|
+
log(message) {
|
|
1079
|
+
return ({ setState }) => {
|
|
1080
|
+
...
|
|
1081
|
+
};
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
const extraActions = actions(useStore, {
|
|
1088
|
+
addToHistory(message) {
|
|
1089
|
+
return ({ setState }) => {
|
|
1090
|
+
...
|
|
1091
|
+
};
|
|
1092
|
+
},
|
|
1093
|
+
|
|
1094
|
+
incrementAndLog(amount = 1) {
|
|
1095
|
+
return ({ setState, actions }) => {
|
|
1096
|
+
setState((s) => ({ ...s, count: s.count + amount }));
|
|
1097
|
+
|
|
1098
|
+
// Call sibling actions with 'this'
|
|
1099
|
+
this.addToHistory(`Incremented by ${amount}`);
|
|
216
1100
|
|
|
217
|
-
|
|
218
|
-
|
|
1101
|
+
// Access store's public actions
|
|
1102
|
+
actions.log(`Count increased by ${amount}`);
|
|
1103
|
+
};
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
extraActions.incrementAndLog(5);
|
|
1108
|
+
```
|
|
219
1109
|
|
|
220
|
-
|
|
1110
|
+
#### 🎪 Works with Context!
|
|
221
1111
|
|
|
222
1112
|
```tsx
|
|
223
|
-
import
|
|
1113
|
+
import { actions, InferAPI } from 'react-hooks-global-states';
|
|
1114
|
+
|
|
1115
|
+
const CounterContext = createContext(0);
|
|
1116
|
+
|
|
1117
|
+
type CounterAPI = InferAPI<typeof CounterContext>;
|
|
1118
|
+
|
|
1119
|
+
// Define action template outside - reusable domain actions
|
|
1120
|
+
const counterActionsTemplate = actions<CounterAPI>()({
|
|
1121
|
+
increment() {
|
|
1122
|
+
return ({ setState, getState }) => {
|
|
1123
|
+
setState(getState() + 1);
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
224
1126
|
|
|
225
|
-
|
|
1127
|
+
decrement() {
|
|
1128
|
+
return ({ setState, getState }) => {
|
|
1129
|
+
setState(getState() - 1);
|
|
1130
|
+
};
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
226
1133
|
|
|
227
|
-
|
|
1134
|
+
function App() {
|
|
228
1135
|
return (
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
</
|
|
1136
|
+
<CounterContext.Provider>
|
|
1137
|
+
<Counter />
|
|
1138
|
+
</CounterContext.Provider>
|
|
232
1139
|
);
|
|
233
|
-
}
|
|
1140
|
+
}
|
|
234
1141
|
|
|
235
|
-
|
|
236
|
-
const
|
|
1142
|
+
function Counter() {
|
|
1143
|
+
const api = CounterContext.use.api();
|
|
237
1144
|
|
|
238
|
-
|
|
239
|
-
};
|
|
1145
|
+
// Bind template to context instance
|
|
1146
|
+
const { increment, decrement } = useMemo(() => counterActionsTemplate(api), [api]);
|
|
240
1147
|
|
|
241
|
-
|
|
242
|
-
|
|
1148
|
+
return (
|
|
1149
|
+
<div>
|
|
1150
|
+
<button onClick={increment}>+1</button>
|
|
1151
|
+
<button onClick={decrement}>-1</button>
|
|
1152
|
+
</div>
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
243
1156
|
|
|
244
|
-
|
|
245
|
-
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
## 🔥 Advanced Patterns
|
|
1160
|
+
|
|
1161
|
+
### 🏗️ Production Architecture (File Organization)
|
|
1162
|
+
|
|
1163
|
+
Real-world large-scale applications need clean separation! Here's how to organize a store with actions in separate files, custom hooks, observables, and a namespace pattern:
|
|
1164
|
+
|
|
1165
|
+
**File Structure:**
|
|
1166
|
+
|
|
1167
|
+
```
|
|
1168
|
+
src/stores/todos/
|
|
1169
|
+
├── index.ts // Namespace - bundles everything
|
|
1170
|
+
├── store.ts // Store definition
|
|
1171
|
+
├── constants/ // Initial state & metadata
|
|
1172
|
+
│ ├── initialValue.ts
|
|
1173
|
+
│ └── metadata.ts
|
|
1174
|
+
├── types/ // Type definitions
|
|
1175
|
+
│ ├── TodosAPI.ts
|
|
1176
|
+
│ └── Todo.ts
|
|
1177
|
+
├── hooks/ // Custom selector hooks
|
|
1178
|
+
│ ├── useActiveTodos.ts
|
|
1179
|
+
│ └── useCompletedTodos.ts
|
|
1180
|
+
├── observables/ // Observable fragments
|
|
1181
|
+
│ └── activeTodos$.ts
|
|
1182
|
+
├── helpers/ // Utility functions
|
|
1183
|
+
│ └── createTodo.ts
|
|
1184
|
+
└── actions/
|
|
1185
|
+
├── index.ts // Export all actions
|
|
1186
|
+
├── addTodo.ts
|
|
1187
|
+
├── toggleTodo.ts
|
|
1188
|
+
└── internal/
|
|
1189
|
+
└── syncWithServer.ts
|
|
246
1190
|
```
|
|
247
1191
|
|
|
248
|
-
|
|
1192
|
+
**Store Definition (`store.ts`):**
|
|
249
1193
|
|
|
250
1194
|
```tsx
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
1195
|
+
import { createGlobalState, actions, type InferAPI } from 'react-hooks-global-states';
|
|
1196
|
+
import { addTodo, toggleTodo, removeTodo } from './actions';
|
|
1197
|
+
import { syncWithServer } from './actions/internal';
|
|
1198
|
+
import { initialValue, metadata } from './constants';
|
|
1199
|
+
|
|
1200
|
+
type TodosAPI = InferAPI<typeof todosStore>;
|
|
1201
|
+
|
|
1202
|
+
// Internal actions template
|
|
1203
|
+
const internalActions = actions<TodosAPI>()({
|
|
1204
|
+
syncWithServer,
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
const todosStore = createGlobalState(initialValue, {
|
|
1208
|
+
metadata,
|
|
1209
|
+
|
|
1210
|
+
// Public actions - exposed to consumers
|
|
1211
|
+
actions: {
|
|
1212
|
+
addTodo,
|
|
1213
|
+
toggleTodo,
|
|
1214
|
+
removeTodo,
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1217
|
+
callbacks: {
|
|
1218
|
+
onInit: (api) => {
|
|
1219
|
+
// Bind and extend with internal actions (not exposed publicly)
|
|
1220
|
+
const { syncWithServer } = internalActions(api);
|
|
1221
|
+
|
|
1222
|
+
// Auto-sync every 30 seconds
|
|
1223
|
+
const interval = setInterval(() => syncWithServer(), 30000);
|
|
1224
|
+
return () => clearInterval(interval);
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Export types (defined in types/ folder)
|
|
1230
|
+
export type { TodosAPI } from './types';
|
|
1231
|
+
|
|
1232
|
+
export default todosStore;
|
|
254
1233
|
```
|
|
255
1234
|
|
|
256
|
-
|
|
1235
|
+
**Type Files (`types/TodosAPI.ts`):**
|
|
257
1236
|
|
|
258
1237
|
```tsx
|
|
259
|
-
|
|
1238
|
+
// types/TodosAPI.ts
|
|
1239
|
+
// prevents circular type references
|
|
1240
|
+
export type TodosAPI = import('../store').TodosAPI;
|
|
260
1241
|
```
|
|
261
1242
|
|
|
262
|
-
|
|
1243
|
+
**Action File (`actions/addTodo.ts`):**
|
|
263
1244
|
|
|
264
|
-
|
|
1245
|
+
```tsx
|
|
1246
|
+
import type { TodosAPI } from '../types';
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Adds a new todo and syncs with server
|
|
1250
|
+
*/
|
|
1251
|
+
function addTodo(this: TodosAPI['actions'], text: string) {
|
|
1252
|
+
return async ({ setState }: TodosAPI): Promise<void> => {
|
|
1253
|
+
const newTodo = {
|
|
1254
|
+
id: crypto.randomUUID(),
|
|
1255
|
+
text,
|
|
1256
|
+
completed: false,
|
|
1257
|
+
createdAt: new Date(),
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
setState((s) => ({
|
|
1261
|
+
...s,
|
|
1262
|
+
todos: [...s.todos, newTodo],
|
|
1263
|
+
}));
|
|
1264
|
+
|
|
1265
|
+
// Call internal action to sync
|
|
1266
|
+
await this.syncWithServer();
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
export default addTodo;
|
|
1271
|
+
```
|
|
265
1272
|
|
|
266
|
-
|
|
1273
|
+
**Custom Hook (`hooks/useActiveTodos.ts`):**
|
|
267
1274
|
|
|
268
|
-
|
|
1275
|
+
```tsx
|
|
1276
|
+
import todosStore from '../store';
|
|
269
1277
|
|
|
270
|
-
|
|
1278
|
+
export const useActiveTodos = todosStore.createSelectorHook((state) =>
|
|
1279
|
+
state.todos.filter((t) => !t.completed),
|
|
1280
|
+
);
|
|
1281
|
+
```
|
|
271
1282
|
|
|
272
|
-
|
|
1283
|
+
**Observable (`observables/activeTodos$.ts`):**
|
|
273
1284
|
|
|
274
1285
|
```tsx
|
|
275
|
-
|
|
276
|
-
|
|
1286
|
+
import { useActiveTodos } from '../hooks/useActiveTodos';
|
|
1287
|
+
|
|
1288
|
+
// Derive from hook - reuses the filter logic
|
|
1289
|
+
export const activeTodos$ = useActiveTodos.createObservable((s) => s);
|
|
277
1290
|
```
|
|
278
1291
|
|
|
279
|
-
|
|
1292
|
+
**Namespace Pattern (`index.ts`):**
|
|
280
1293
|
|
|
281
1294
|
```tsx
|
|
282
|
-
|
|
283
|
-
|
|
1295
|
+
import store from './store';
|
|
1296
|
+
import { useActiveTodos, useCompletedTodos } from './hooks';
|
|
1297
|
+
import { activeTodos$, completedTodos$ } from './observables';
|
|
1298
|
+
|
|
1299
|
+
// Bundle everything into a clean namespace
|
|
1300
|
+
const todos$ = Object.assign(store, {
|
|
1301
|
+
// Custom hooks
|
|
1302
|
+
useActiveTodos,
|
|
1303
|
+
useCompletedTodos,
|
|
1304
|
+
|
|
1305
|
+
// Observables
|
|
1306
|
+
activeTodos$,
|
|
1307
|
+
completedTodos$,
|
|
284
1308
|
});
|
|
1309
|
+
|
|
1310
|
+
export default todos$;
|
|
285
1311
|
```
|
|
286
1312
|
|
|
287
|
-
|
|
1313
|
+
**Usage:**
|
|
288
1314
|
|
|
289
1315
|
```tsx
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1316
|
+
import todos$ from './stores/todos';
|
|
1317
|
+
|
|
1318
|
+
function TodoApp() {
|
|
1319
|
+
// Use the namespace
|
|
1320
|
+
const activeTodos = todos$.useActiveTodos();
|
|
1321
|
+
|
|
1322
|
+
// Subscribe to observable outside React
|
|
1323
|
+
useEffect(() => {
|
|
1324
|
+
const sub = todos$.activeTodos$.subscribe((todos) => {
|
|
1325
|
+
console.log('Active todos changed:', todos.length);
|
|
1326
|
+
});
|
|
1327
|
+
return () => sub();
|
|
1328
|
+
}, []);
|
|
1329
|
+
|
|
1330
|
+
return (
|
|
1331
|
+
<div>
|
|
1332
|
+
<button onClick={() => todos$.actions.addTodo('New task')}>Add</button>
|
|
1333
|
+
{activeTodos.map((todo) => (
|
|
1334
|
+
<div key={todo.id}>{todo.text}</div>
|
|
1335
|
+
))}
|
|
1336
|
+
</div>
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
**Why this pattern rocks:**
|
|
294
1342
|
|
|
295
|
-
|
|
296
|
-
|
|
1343
|
+
- ✅ **KISS** - Keep It Simple, Stupid! Easy to navigate
|
|
1344
|
+
- ✅ **Type-safe** - Everything is strongly typed
|
|
1345
|
+
- ✅ **Public/Private APIs** - Internal actions don't pollute public interface
|
|
1346
|
+
- ✅ **Namespace pattern** - Everything bundled: `todos$.useActiveTodos()`, `todos$.activeTodos$`
|
|
1347
|
+
- ✅ **Scalable** - Easy to find, test, and maintain individual pieces
|
|
1348
|
+
|
|
1349
|
+
### 🎧 Smart Subscriptions
|
|
1350
|
+
|
|
1351
|
+
Subscribe to specific slices outside React!
|
|
1352
|
+
|
|
1353
|
+
```tsx
|
|
1354
|
+
const useStore = createGlobalState({
|
|
1355
|
+
user: { name: 'John', role: 'admin' },
|
|
1356
|
+
theme: 'dark',
|
|
1357
|
+
notifications: [],
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// Subscribe to just the theme
|
|
1361
|
+
const unsubTheme = useStore.subscribe(
|
|
1362
|
+
(state) => state.theme,
|
|
1363
|
+
(theme) => {
|
|
1364
|
+
document.body.className = theme;
|
|
1365
|
+
localStorage.setItem('theme', theme);
|
|
1366
|
+
},
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
// Subscribe to user role changes
|
|
1370
|
+
const unsubRole = useStore.subscribe(
|
|
1371
|
+
(state) => state.user.role,
|
|
1372
|
+
(role) => {
|
|
1373
|
+
console.log('User role changed to:', role);
|
|
1374
|
+
analytics.track('role_changed', { role });
|
|
1375
|
+
},
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
// Cleanup
|
|
1379
|
+
unsubTheme();
|
|
1380
|
+
unsubRole();
|
|
297
1381
|
```
|
|
298
1382
|
|
|
299
1383
|
---
|
|
300
1384
|
|
|
301
|
-
##
|
|
1385
|
+
## 🎨 `uniqueId` - Type-Safe Unique IDs
|
|
302
1386
|
|
|
303
|
-
|
|
304
|
-
| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
305
|
-
| **Scope** | Available globally across the entire app | Scoped to the Provider where it’s used |
|
|
306
|
-
| **How to Use** | `const useCount = createGlobalState(0)` | `const counter = createContext(0); counter.Provider, counter.use()` |
|
|
307
|
-
| **createSelectorHook** | `useCount.createSelectorHook` | `counter.use.createSelectorHook()` |
|
|
308
|
-
| **inline selectors?** | ✅ Supported | ✅ Supported |
|
|
309
|
-
| **Custom Actions** | ✅ Supported | ✅ Supported |
|
|
310
|
-
| **Observables** | `useCount.createObservable` | `counter.api().createObservable()` |
|
|
311
|
-
| **Best For** | Global app state (auth, settings, cache) | Scoped module state, reusable component state, or state shared between child components without being fully global |
|
|
1387
|
+
Generate branded unique identifiers with compile-time safety!
|
|
312
1388
|
|
|
313
|
-
|
|
1389
|
+
### 🏷️ Basic Usage
|
|
314
1390
|
|
|
315
|
-
|
|
1391
|
+
```tsx
|
|
1392
|
+
import { uniqueId } from 'react-hooks-global-states';
|
|
1393
|
+
|
|
1394
|
+
// Simple IDs
|
|
1395
|
+
const id1 = uniqueId(); // "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1396
|
+
const id2 = uniqueId('user:'); // "user:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1397
|
+
const id3 = uniqueId('session:'); // "session:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
### 🔒 Branded IDs (Type Safety!)
|
|
1401
|
+
|
|
1402
|
+
Create ID generators with compile-time type checking!
|
|
316
1403
|
|
|
317
1404
|
```tsx
|
|
318
|
-
|
|
319
|
-
|
|
1405
|
+
// Create branded generators
|
|
1406
|
+
const generateUserId = uniqueId.for('user:');
|
|
1407
|
+
const generatePostId = uniqueId.for('post:');
|
|
1408
|
+
|
|
1409
|
+
type UserId = ReturnType<typeof generateUserId>;
|
|
1410
|
+
type PostId = ReturnType<typeof generatePostId>;
|
|
1411
|
+
|
|
1412
|
+
const userId: UserId = generateUserId(); // ✅ "user:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1413
|
+
const postId: PostId = generatePostId(); // ✅ "post:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1414
|
+
|
|
1415
|
+
// TypeScript prevents mixing!
|
|
1416
|
+
const wrong: UserId = generatePostId(); // ❌ Type error!
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
### 🛡️ Runtime Validation
|
|
1420
|
+
|
|
1421
|
+
```tsx
|
|
1422
|
+
const generateUserId = uniqueId.for('user:');
|
|
1423
|
+
|
|
1424
|
+
const id = generateUserId(); // "user:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1425
|
+
|
|
1426
|
+
// Check if a string is a valid user ID
|
|
1427
|
+
if (generateUserId.is(id)) {
|
|
1428
|
+
console.log('Valid user ID!');
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
generateUserId.is('user:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ✅ true
|
|
1432
|
+
generateUserId.is('post:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ❌ false
|
|
1433
|
+
|
|
1434
|
+
// Assert (throws if invalid)
|
|
1435
|
+
generateUserId.assert('user:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ✅ OK
|
|
1436
|
+
generateUserId.assert('post:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ❌ Throws error!
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
### 🎯 Strict Branding
|
|
1440
|
+
|
|
1441
|
+
Maximum type safety with symbol branding!
|
|
1442
|
+
|
|
1443
|
+
```tsx
|
|
1444
|
+
declare const UserBrand: unique symbol;
|
|
1445
|
+
declare const PostBrand: unique symbol;
|
|
1446
|
+
|
|
1447
|
+
const generateUserId = uniqueId.for('user:').strict<typeof UserBrand>();
|
|
1448
|
+
const generatePostId = uniqueId.for('post:').strict<typeof PostBrand>();
|
|
1449
|
+
|
|
1450
|
+
// Even with same prefix, types are incompatible!
|
|
1451
|
+
const generateUserId2 = uniqueId.for('user:').strict<typeof PostBrand>();
|
|
1452
|
+
|
|
1453
|
+
type UserId = ReturnType<typeof generateUserId>;
|
|
1454
|
+
type UserId2 = ReturnType<typeof generateUserId2>;
|
|
1455
|
+
|
|
1456
|
+
const id1: UserId = generateUserId(); // ✅
|
|
1457
|
+
const id2: UserId = generateUserId2(); // ❌ Different brands!
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
### 💼 Real-World Example
|
|
1461
|
+
|
|
1462
|
+
```tsx
|
|
1463
|
+
import { createGlobalState, uniqueId } from 'react-hooks-global-states';
|
|
1464
|
+
|
|
1465
|
+
// Create typed ID generators
|
|
1466
|
+
const generateUserId = uniqueId.for('user:');
|
|
1467
|
+
const generateTodoId = uniqueId.for('todo:');
|
|
1468
|
+
|
|
1469
|
+
type UserId = ReturnType<typeof generateUserId>;
|
|
1470
|
+
type TodoId = ReturnType<typeof generateTodoId>;
|
|
1471
|
+
|
|
1472
|
+
interface User {
|
|
1473
|
+
id: UserId;
|
|
1474
|
+
name: string;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
interface Todo {
|
|
1478
|
+
id: TodoId;
|
|
1479
|
+
text: string;
|
|
1480
|
+
assignedTo: UserId | null;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const useApp = createGlobalState(
|
|
320
1484
|
{
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1485
|
+
users: [] as User[],
|
|
1486
|
+
todos: [] as Todo[],
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
actions: {
|
|
1490
|
+
addUser(name: string) {
|
|
1491
|
+
return ({ setState, getState }) => {
|
|
1492
|
+
const user: User = {
|
|
1493
|
+
id: generateUserId(), // Type-safe!
|
|
1494
|
+
name,
|
|
1495
|
+
};
|
|
1496
|
+
setState((s) => ({
|
|
1497
|
+
...s,
|
|
1498
|
+
users: [...s.users, user],
|
|
1499
|
+
}));
|
|
1500
|
+
};
|
|
324
1501
|
},
|
|
325
|
-
|
|
326
|
-
|
|
1502
|
+
|
|
1503
|
+
addTodo(text: string, assignedTo: UserId | null = null) {
|
|
1504
|
+
return ({ setState, getState }) => {
|
|
1505
|
+
const todo: Todo = {
|
|
1506
|
+
id: generateTodoId(), // Type-safe!
|
|
1507
|
+
text,
|
|
1508
|
+
assignedTo,
|
|
1509
|
+
};
|
|
1510
|
+
setState((s) => ({
|
|
1511
|
+
...s,
|
|
1512
|
+
todos: [...s.todos, todo],
|
|
1513
|
+
}));
|
|
1514
|
+
};
|
|
327
1515
|
},
|
|
328
|
-
|
|
329
|
-
|
|
1516
|
+
|
|
1517
|
+
assignTodo(todoId: TodoId, userId: UserId) {
|
|
1518
|
+
return ({ setState, getState }) => {
|
|
1519
|
+
// TypeScript ensures correct ID types!
|
|
1520
|
+
setState((s) => ({
|
|
1521
|
+
...s,
|
|
1522
|
+
todos: s.todos.map((t) => (t.id === todoId ? { ...t, assignedTo: userId } : t)),
|
|
1523
|
+
}));
|
|
1524
|
+
};
|
|
330
1525
|
},
|
|
331
1526
|
},
|
|
332
1527
|
},
|
|
333
1528
|
);
|
|
1529
|
+
|
|
1530
|
+
// Usage
|
|
1531
|
+
const [, actions] = useApp();
|
|
1532
|
+
|
|
1533
|
+
// Create a new user
|
|
1534
|
+
const userId = generateUserId();
|
|
1535
|
+
|
|
1536
|
+
actions.addUser('John');
|
|
1537
|
+
actions.addTodo('Build feature', userId);
|
|
334
1538
|
```
|
|
335
1539
|
|
|
336
|
-
|
|
1540
|
+
---
|
|
1541
|
+
|
|
1542
|
+
## 🎓 Learning Resources
|
|
337
1543
|
|
|
338
|
-
|
|
1544
|
+
| Resource | Description |
|
|
1545
|
+
| ------------------------------------------------------------------------------------ | --------------------------------- |
|
|
1546
|
+
| 🎮 [**Live Demo**](https://johnny-quesada-developer.github.io/global-hooks-example/) | Interactive examples |
|
|
1547
|
+
| 🎥 [**Video Tutorial**](https://www.youtube.com/watch?v=1UBqXk2MH8I/) | Full walkthrough |
|
|
1548
|
+
| 💻 [**CodePen**](https://codepen.io/johnnynabetes/pen/WNmeGwb?editors=0010) | Try it online |
|
|
1549
|
+
| 📚 **400+ Tests** | Check the test suite for patterns |
|
|
339
1550
|
|
|
340
|
-
|
|
1551
|
+
---
|
|
1552
|
+
|
|
1553
|
+
## 🌐 Platform-Specific Versions
|
|
1554
|
+
|
|
1555
|
+
| Package | Platform | Special Feature |
|
|
1556
|
+
| -------------------------------------------------------------------------------------------------- | -------------------- | ------------------------ |
|
|
1557
|
+
| [`react-hooks-global-states`](https://www.npmjs.com/package/react-hooks-global-states) | React / React Native | Core library |
|
|
1558
|
+
| [`react-global-state-hooks`](https://www.npmjs.com/package/react-global-state-hooks) | Web | localStorage integration |
|
|
1559
|
+
| [`react-native-global-state-hooks`](https://www.npmjs.com/package/react-native-global-state-hooks) | React Native | AsyncStorage integration |
|
|
1560
|
+
|
|
1561
|
+
---
|
|
1562
|
+
|
|
1563
|
+
## 🎉 Why Developers Choose This
|
|
341
1564
|
|
|
342
1565
|
```tsx
|
|
343
|
-
|
|
1566
|
+
"I replaced 500 lines of Redux with 50 lines of this. Mind blown." 🤯
|
|
1567
|
+
- Every developer who tries it
|
|
1568
|
+
|
|
1569
|
+
"The useState I always wanted." ❤️
|
|
1570
|
+
- React developers everywhere
|
|
1571
|
+
|
|
1572
|
+
"Finally, state management that doesn't fight me." 🥊
|
|
1573
|
+
- Tired developers worldwide
|
|
344
1574
|
```
|
|
345
1575
|
|
|
346
|
-
|
|
1576
|
+
### The Bottom Line
|
|
1577
|
+
|
|
1578
|
+
| What You Get | What You Don't |
|
|
1579
|
+
| --------------------------- | --------------------- |
|
|
1580
|
+
| ✅ `useState` API | ❌ Boilerplate |
|
|
1581
|
+
| ✅ Surgical re-renders | ❌ Whole-tree updates |
|
|
1582
|
+
| ✅ Chainable selectors | ❌ Repetitive code |
|
|
1583
|
+
| ✅ TypeScript inference | ❌ Manual typing |
|
|
1584
|
+
| ✅ Global + Context | ❌ Either/or choice |
|
|
1585
|
+
| ✅ Actions (optional) | ❌ Required structure |
|
|
1586
|
+
| ✅ 30-second learning curve | ❌ Week-long training |
|
|
1587
|
+
|
|
1588
|
+
---
|
|
1589
|
+
|
|
1590
|
+
## 🚀 Get Started Now
|
|
1591
|
+
|
|
1592
|
+
```bash
|
|
1593
|
+
npm install react-hooks-global-states
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
Then in your app:
|
|
347
1597
|
|
|
348
1598
|
```tsx
|
|
349
|
-
|
|
1599
|
+
import { createGlobalState } from 'react-hooks-global-states';
|
|
1600
|
+
|
|
1601
|
+
const useTheme = createGlobalState('light');
|
|
350
1602
|
|
|
351
|
-
|
|
1603
|
+
function App() {
|
|
1604
|
+
const [theme, setTheme] = useTheme();
|
|
1605
|
+
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle Theme</button>;
|
|
1606
|
+
}
|
|
352
1607
|
```
|
|
353
1608
|
|
|
354
|
-
|
|
1609
|
+
**That's it. You're done.** 🎉
|
|
1610
|
+
|
|
1611
|
+
---
|
|
1612
|
+
|
|
1613
|
+
<div align="center">
|
|
1614
|
+
|
|
1615
|
+
### Built with ❤️ for developers who value simplicity
|
|
355
1616
|
|
|
356
|
-
|
|
1617
|
+
**[⭐ Star on GitHub](https://github.com/johnny-quesada-developer/react-hooks-global-states)** • **[📝 Report Issues](https://github.com/johnny-quesada-developer/react-hooks-global-states/issues)** • **[💬 Discussions](https://github.com/johnny-quesada-developer/react-hooks-global-states/discussions)**
|
|
357
1618
|
|
|
358
|
-
|
|
1619
|
+
</div>
|