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/README.md CHANGED
@@ -2,357 +2,1618 @@
2
2
 
3
3
  ![Image John Avatar](https://raw.githubusercontent.com/johnny-quesada-developer/global-hooks-example/main/public/avatar2.jpeg)
4
4
 
5
- Effortless **global state management** for `React` & `React Native` & `Preact`! 🚀 Define a **global state in just one line of code** and enjoy **lightweight, flexible, and scalable** state management. Try it now on **[CodePen](https://codepen.io/johnnynabetes/pen/WNmeGwb?editors=0010)** and see it in action! ✨
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
- ## 🔗 Explore More
11
+ [![npm version](https://img.shields.io/npm/v/react-hooks-global-states.svg)](https://www.npmjs.com/package/react-hooks-global-states)
12
+ [![Downloads](https://img.shields.io/npm/dm/react-hooks-global-states.svg)](https://www.npmjs.com/package/react-hooks-global-states)
13
+ [![License](https://img.shields.io/npm/l/react-hooks-global-states.svg)](https://github.com/johnny-quesada-developer/react-hooks-global-states/blob/main/LICENSE)
10
14
 
11
- - **[Live Example](https://johnny-quesada-developer.github.io/global-hooks-example/)** 📘
12
- - **[Video Overview](https://www.youtube.com/watch?v=1UBqXk2MH8I/)** 🎥
13
- - **[react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)** compatible with both `React & React Native`
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
- ## 🚀 React Hooks Global States - DevTools Extension
21
+ ## 🎯 The One-Liner
20
22
 
21
- React Hooks Global States includes a dedicated, `devTools extension` to streamline your development workflow! Easily visualize, inspect, debug, and modify your application's global state in real-time right within your browser.
23
+ ```tsx
24
+ import { createGlobalState } from 'react-hooks-global-states';
22
25
 
23
- ### 🔗 [Install the DevTools Extension for Chrome](https://chromewebstore.google.com/detail/bafojplmkpejhglhjpibpdhoblickpee/preview?hl=en&authuser=0)
26
+ export const useCounter = createGlobalState(0);
27
+ ```
24
28
 
25
- ### 📸 DevTools Highlights
29
+ **That's it.** No providers. No context boilerplate. No configuration files. Just pure, beautiful state management. 🎨
26
30
 
27
- | **Track State Changes** | **Modify the State** |
28
- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
29
- | ![Track State Changes](https://github.com/johnny-quesada-developer/react-hooks-global-states/raw/main/public/track-state-changes.png) | ![Modify the State](https://github.com/johnny-quesada-developer/react-hooks-global-states/raw/main/public/modify-the-state.png) |
30
- | Effortlessly monitor state updates and history. | Instantly edit global states directly from the extension. |
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
- | **Restore the State** | **Custom Actions Granularity** |
35
- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
36
- | ![Restore the State](https://github.com/johnny-quesada-developer/react-hooks-global-states/raw/main/public/restore-the-state.png) | ![Custom Actions Granularity](https://github.com/johnny-quesada-developer/react-hooks-global-states/raw/main/public/custom-actions-granularity.png) |
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
- ## 🛠 Creating a Global State
43
+ <table>
44
+ <tr>
45
+ <td width="50%">
42
46
 
43
- Define a **global state** in **one line**:
47
+ ### 🎓 **Zero Learning Curve**
44
48
 
45
49
  ```tsx
46
- import createGlobalState from 'react-hooks-global-states/createGlobalState';
47
- export const useCount = createGlobalState(0);
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
- Now, use it inside a component:
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
- const [count, setCount] = useCount();
54
- return <Button onClick={() => setCount((count) => count + 1)}>{count}</Button>;
65
+ // Only re-renders when name changes
66
+ const [name] = useStore((s) => s.user.name);
55
67
  ```
56
68
 
57
- Works just like **useState**, but the **state is shared globally**! 🎉
69
+ </td>
70
+ </tr>
58
71
 
59
- ---
72
+ <tr>
73
+ <td width="50%">
60
74
 
61
- ## 🎯 Selectors: Subscribing to Specific State Changes
75
+ ### 🔗 **Chainable Selectors**
62
76
 
63
- For **complex state objects**, you can subscribe to specific properties instead of the entire state:
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
- export const useContacts = createGlobalState({ entities: [], selected: new Set<number>() });
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
- To access only the `entities` property:
100
+ </td>
101
+ </tr>
102
+
103
+ <tr>
104
+ <td width="50%">
105
+
106
+ ### 🎪 **Context Mode**
70
107
 
71
108
  ```tsx
72
- const [contacts] = useContacts((state) => state.entities);
73
- return (
74
- <ul>
75
- {contacts.map((contact) => (
76
- <li key={contact.id}>{contact.name}</li>
77
- ))}
78
- </ul>
79
- );
109
+ const Form = createContext({ name: '', email: '' });
110
+
111
+ <Form.Provider>
112
+ <FormFields />
113
+ </Form.Provider>;
80
114
  ```
81
115
 
82
- ### 📌 Using Dependencies in Selectors
116
+ </td>
117
+ <td width="50%">
83
118
 
84
- You can also add **dependencies** to a selector. This is useful when you want to derive state based on another piece of state (e.g., a filtered list). For example, if you're filtering contacts based on a `filter` value:
119
+ ### � **Non-Reactive API**
120
+
121
+ Use state anywhere - even outside React components!
85
122
 
86
123
  ```tsx
87
- const [contacts] = useContacts(
88
- (state) => state.entities.filter((item) => item.name.includes(filter)),
89
- [filter],
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
- Alternatively, you can pass dependencies inside an **options object**:
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
- const [contacts] = useContacts((state) => state.entities.filter((item) => item.name.includes(filter)), {
97
- dependencies: [filter],
98
- isEqualRoot: (a, b) => a.entities === b.entities,
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
- Unlike Redux, where only **root state changes trigger re-selection**, this approach ensures that **derived values recompute when dependencies change** while maintaining performance.
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
- ## 🔄 Reusing Selectors
229
+ ## 🌟 Core Features Deep Dive
230
+
231
+ ### 1️⃣ Global State with `createGlobalState`
107
232
 
108
- ### 📌 Creating a Selector
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
- export const useContactsArray = useContacts.createSelectorHook((state) => state.entities);
112
- export const useContactsCount = useContactsArray.createSelectorHook((entities) => entities.length);
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
- ### 📌 Using Selectors in Components
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 [contacts] = useContactsArray();
119
- const [count] = useContactsCount();
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
- #### ✅ Selectors support inline selectors and dependencies
300
+ **Performance comparison:**
123
301
 
124
- You can still **use dependencies** inside a selector hook:
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 [filteredContacts] = useContactsArray(
128
- (contacts) => contacts.filter((c) => c.name.includes(filter)),
129
- [filter],
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
- #### ✅ Selector hooks share the same state mutator
385
+ #### 🔗 Reusable Selector Hooks (The Game Changer!)
134
386
 
135
- The **state api is stable across renders** meaning actions and setState functions stay consistent.
387
+ Create hooks from hooks! Chain them! Compose them! This is where it gets fun! 🎉
136
388
 
137
389
  ```tsx
138
- const [, setState1] = useContactsArray();
139
- const [contacts] = useContactsCount();
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
- console.log(setState1 === useContacts.setState); // true
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
- ## 🎛 State Actions: Controlling State Modifications
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
- Restrict **state modifications** by defining custom actions:
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
- export const useContacts = createGlobalState(
152
- { filter: '', items: [] },
460
+ const useStore = createGlobalState(
461
+ {
462
+ todos: new Map(), // Map<id, todo>
463
+ filter: 'all',
464
+ },
153
465
  {
154
466
  actions: {
155
- async fetch() {
156
- return async ({ setState }) => {
157
- const items = await fetchItems();
158
- setState({ items });
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
- setFilter(filter: string) {
162
- return ({ setState }) => {
163
- setState((state) => ({ ...state, filter }));
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
- Now, instead of `setState`, the hook returns **actions**:
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 [filter, { setFilter }] = useContacts();
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
- ## 🌍 Accessing Global State Outside Components
673
+ ### 2️⃣ Scoped State with `createContext`
180
674
 
181
- You can access and manipulate global without hooks, useful for non-component code like services or utilities.
182
- Or for non reactive components.
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
- console.log(useContacts.getState()); // Retrieves the current state
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
- #### ✅ Subscribe to changes
719
+ #### 🎁 Provider Variations
189
720
 
190
721
  ```tsx
191
- const unsubscribe = useContacts.subscribe((state) => {
192
- console.log('State updated:', state);
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
- #### ✅ Subscriptions are great when one state depends on another.
887
+ #### 🎬 Lifecycle Hooks
888
+
889
+ React to context lifecycle events!
197
890
 
198
891
  ```tsx
199
- const useSelectedContact = createGlobalState(null, {
892
+ const DataContext = createContext([], {
200
893
  callbacks: {
201
- onInit: ({ setState, getState }) => {
202
- useContacts.subscribe(
203
- (state) => state.contacts,
204
- (contacts) => {
205
- if (!contacts.has(getState())) setState(null);
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
- ## 🎭 Using Context for Scoped State
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
- - **Scoped State** – Context state is **isolated inside the provider**.
218
- - **Same API** – Context supports **selectors, actions, and state controls**.
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
- ### 📌 Creating a Context
1110
+ #### 🎪 Works with Context!
221
1111
 
222
1112
  ```tsx
223
- import createContext from 'react-global-state-hooks/createContext';
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
- export const counter = createContext(0);
1127
+ decrement() {
1128
+ return ({ setState, getState }) => {
1129
+ setState(getState() - 1);
1130
+ };
1131
+ },
1132
+ });
226
1133
 
227
- export const App = () => {
1134
+ function App() {
228
1135
  return (
229
- <counter.Provider>
230
- <MyComponent />
231
- </counter.Provider>
1136
+ <CounterContext.Provider>
1137
+ <Counter />
1138
+ </CounterContext.Provider>
232
1139
  );
233
- };
1140
+ }
234
1141
 
235
- export const Component = () => {
236
- const [count, setCount] = counter.use();
1142
+ function Counter() {
1143
+ const api = CounterContext.use.api();
237
1144
 
238
- return <Button onClick={() => setCount((c) => c + 1)}>{count}</Button>;
239
- };
1145
+ // Bind template to context instance
1146
+ const { increment, decrement } = useMemo(() => counterActionsTemplate(api), [api]);
240
1147
 
241
- export const Component2 = () => {
242
- const [count, setCount] = counter.use.api(); // non reactive access to the context api
1148
+ return (
1149
+ <div>
1150
+ <button onClick={increment}>+1</button>
1151
+ <button onClick={decrement}>-1</button>
1152
+ </div>
1153
+ );
1154
+ }
1155
+ ```
243
1156
 
244
- return <Button onClick={() => setCount((c) => c + 1)}>{count}</Button>;
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
- Wrap your app:
1192
+ **Store Definition (`store.ts`):**
249
1193
 
250
1194
  ```tsx
251
- <CounterProvider>
252
- <MyComponent />
253
- </CounterProvider>
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
- Use the context state:
1235
+ **Type Files (`types/TodosAPI.ts`):**
257
1236
 
258
1237
  ```tsx
259
- const [count] = useCounterContext();
1238
+ // types/TodosAPI.ts
1239
+ // prevents circular type references
1240
+ export type TodosAPI = import('../store').TodosAPI;
260
1241
  ```
261
1242
 
262
- ### 📌 Context Selectors
1243
+ **Action File (`actions/addTodo.ts`):**
263
1244
 
264
- Works **just like global state**, but within the provider.
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
- ## 🔥 Observables: Watching State Changes
1275
+ ```tsx
1276
+ import todosStore from '../store';
269
1277
 
270
- Observables **let you react to state changes** via subscriptions.
1278
+ export const useActiveTodos = todosStore.createSelectorHook((state) =>
1279
+ state.todos.filter((t) => !t.completed),
1280
+ );
1281
+ ```
271
1282
 
272
- ### 📌 Creating an Observable
1283
+ **Observable (`observables/activeTodos$.ts`):**
273
1284
 
274
1285
  ```tsx
275
- export const useCounter = createGlobalState(0);
276
- export const counterLogs = useCounter.createObservable((count) => `Counter is at ${count}`);
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
- ### 📌 Subscribing to an Observable
1292
+ **Namespace Pattern (`index.ts`):**
280
1293
 
281
1294
  ```tsx
282
- const unsubscribe = counterLogs((message) => {
283
- console.log(message);
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
- ### 📌 Using Observables Inside Context
1313
+ **Usage:**
288
1314
 
289
1315
  ```tsx
290
- useEffect(() => {
291
- const unsubscribe = useCounterContext.subscribe((count) => {
292
- console.log(`Updated count: ${count}`);
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
- return unsubscribe;
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
- ## ⚖️ `createGlobalState` vs. `createContext`
1385
+ ## 🎨 `uniqueId` - Type-Safe Unique IDs
302
1386
 
303
- | Feature | `createGlobalState` | `createContext` |
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
- ## 🔄 Lifecycle Methods
1389
+ ### 🏷️ Basic Usage
314
1390
 
315
- Global state hooks support lifecycle callbacks for additional control.
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
- const useData = createGlobalState(
319
- { value: 1 },
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
- callbacks: {
322
- onInit: ({ setState }) => {
323
- console.log('Store initialized');
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
- onStateChanged: ({ state, previousState }) => {
326
- console.log('State changed:', previousState, '→', state);
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
- computePreventStateChange: ({ state, previousState }) => {
329
- return state.value === previousState.value;
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
- Use **`onInit`** for setup, **`onStateChanged`** to listen to updates, and **`computePreventStateChange`** to prevent unnecessary updates.
1540
+ ---
1541
+
1542
+ ## 🎓 Learning Resources
337
1543
 
338
- ## Metadata
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
- There is a possibility to add non reactive information in the global state:
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
- const useCount = createGlobalState(0, { metadata: { renders: 0 } });
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
- How to use it?
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
- const [count, , metadata] = useCount();
1599
+ import { createGlobalState } from 'react-hooks-global-states';
1600
+
1601
+ const useTheme = createGlobalState('light');
350
1602
 
351
- metadata.renders += 1;
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
- ## 🎯 Ready to Try It?
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
- 📦 **NPM Package:** [react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)
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
- 🚀 Simplify your **global state management** in React & React Native today! 🚀
1619
+ </div>