react-hooks-global-states 15.0.12 → 15.0.14

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.
@@ -4,3 +4,4 @@ import type { CreateGlobalState } from './types';
4
4
  */
5
5
  export declare const createGlobalState: CreateGlobalState;
6
6
  export default createGlobalState;
7
+ export type { InferActionsType, InferStateApi, AnyActions } from './types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-hooks-global-states",
3
- "version": "15.0.12",
3
+ "version": "15.0.14",
4
4
  "description": "A package to easily handle global state across your React components using hooks.",
5
5
  "main": "./bundle.js",
6
6
  "types": "./index.d.ts",
@@ -1,1191 +0,0 @@
1
- # react-hooks-global-states 🌟
2
-
3
- ![Image John Avatar](https://raw.githubusercontent.com/johnny-quesada-developer/global-hooks-example/main/public/avatar2.jpeg)
4
-
5
- **Effortless global state management for React, React Native, and Preact!** 🚀
6
-
7
- Define **global state in just one line of code** and enjoy **lightweight, flexible, and scalable** state management with the familiarity of `useState`, but with the power of Redux and the simplicity of hooks. Zero configuration, fully typed, and framework-agnostic! ✨
8
-
9
- ```tsx
10
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
11
-
12
- export const useCounter = createGlobalState(0);
13
-
14
- // That's it! Use it anywhere in your app 🎉
15
- function Counter() {
16
- const [count, setCount] = useCounter();
17
- return <button onClick={() => setCount(count + 1)}>{count}</button>;
18
- }
19
- ```
20
-
21
- ---
22
-
23
- ## 🔗 Explore More
24
-
25
- - **[Live Example](https://johnny-quesada-developer.github.io/global-hooks-example/)** 📘
26
- - **[Video Overview](https://www.youtube.com/watch?v=1UBqXk2MH8I/)** 🎥
27
- - **[CodePen Demo](https://codepen.io/johnnynabetes/pen/WNmeGwb?editors=0010)** - Try it now!
28
- - **[react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)** - Compatible with React & React Native
29
- - **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** - Web with **localStorage integration**
30
- - **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** - React Native with **AsyncStorage integration**
31
-
32
- ---
33
-
34
- ## 🚀 DevTools Extension
35
-
36
- 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 within your browser.
37
-
38
- ### 🔗 [Install the DevTools Extension for Chrome](https://chromewebstore.google.com/detail/bafojplmkpejhglhjpibpdhoblickpee/preview?hl=en&authuser=0)
39
-
40
- ### 📸 DevTools Highlights
41
-
42
- | **Track State Changes** | **Modify the State** |
43
- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
44
- | ![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) |
45
- | Effortlessly monitor state updates and history. | Instantly edit global states directly from the extension. |
46
-
47
- ---
48
-
49
- | **Restore the State** | **Custom Actions Granularity** |
50
- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | ![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) |
52
- | Quickly revert your application to a previous state. | Precisely debug specific actions affecting state changes. |
53
-
54
- ---
55
-
56
- ## 🎯 Why This Library?
57
-
58
- | Feature | react-hooks-global-states | Redux | Zustand | Jotai |
59
- | -------------------------- | -------------------------------------- | ---------------------- | ----------------- | ----------------------- |
60
- | **Setup complexity** | ✅ One line | ❌ Boilerplate heavy | ✅ Minimal | ✅ Minimal |
61
- | **TypeScript** | ✅ Full inference | ⚠️ Manual typing | ✅ Good | ✅ Good |
62
- | **Selectors** | ✅ Built-in + chainable | ⚠️ Separate lib needed | ✅ Built-in | ✅ Built-in |
63
- | **Actions/Reducers** | ✅ Optional, type-safe | ✅ Required | ✅ Optional | ❌ Not built-in |
64
- | **DevTools** | ✅ Dedicated extension | ✅ Redux DevTools | ⚠️ Via middleware | ⚠️ Via additional setup |
65
- | **Scoped state (Context)** | ✅ Built-in API | ❌ | ⚠️ Manual | ✅ Via atoms |
66
- | **Observable patterns** | ✅ First-class | ❌ | ⚠️ Via subscribe | ⚠️ Custom |
67
- | **Dependencies array** | ✅ Selector deps | ❌ | ❌ | ❌ |
68
- | **Learning curve** | ✅ If you know useState, you know this | ❌ High | ✅ Low | ✅ Low |
69
-
70
- ---
71
-
72
- ## 📦 Installation
73
-
74
- ```bash
75
- npm install react-hooks-global-states
76
- # or
77
- yarn add react-hooks-global-states
78
- # or
79
- pnpm add react-hooks-global-states
80
- ```
81
-
82
- **Platform-specific packages with storage integration:**
83
-
84
- - **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** - Web with localStorage
85
- - **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** - React Native with AsyncStorage
86
-
87
- ---
88
-
89
- ## 🚀 Quick Start
90
-
91
- ### Basic Usage
92
-
93
- ```tsx
94
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
95
-
96
- // Create a global state
97
- const useAuth = createGlobalState<{ user: User | null }>({ user: null });
98
-
99
- // Use it in any component
100
- function UserProfile() {
101
- const [auth, setAuth] = useAuth();
102
-
103
- if (!auth.user) return <Login />;
104
-
105
- return <div>Welcome {auth.user.name}</div>;
106
- }
107
-
108
- function LoginButton() {
109
- const [, setAuth] = useAuth();
110
-
111
- return <button onClick={() => setAuth({ user: { name: 'John' } })}>Login</button>;
112
- }
113
- ```
114
-
115
- ---
116
-
117
- ## 🎛️ Core Features
118
-
119
- ### 1️⃣ Selectors - Subscribe to Specific State Changes
120
-
121
- For complex state objects, selectors allow components to subscribe only to the parts of state they care about, preventing unnecessary re-renders.
122
-
123
- ```tsx
124
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
125
-
126
- const useContacts = createGlobalState({
127
- items: [] as Contact[],
128
- filter: '',
129
- selectedIds: new Set<number>(),
130
- });
131
-
132
- // Component only re-renders when items change
133
- function ContactList() {
134
- const [contacts] = useContacts((state) => state.items);
135
-
136
- return (
137
- <ul>
138
- {contacts.map((contact) => (
139
- <li key={contact.id}>{contact.name}</li>
140
- ))}
141
- </ul>
142
- );
143
- }
144
-
145
- // Component only re-renders when filter changes
146
- function SearchBar() {
147
- const [filter, setContacts] = useContacts(
148
- (state) => state.filter,
149
- // Second parameter can customize equality checks
150
- { isEqualRoot: (a, b) => a.filter === b.filter },
151
- );
152
-
153
- return (
154
- <input value={filter} onChange={(e) => setContacts((prev) => ({ ...prev, filter: e.target.value }))} />
155
- );
156
- }
157
- ```
158
-
159
- ### 2️⃣ Derived Selectors with Dependencies
160
-
161
- Unlike Redux, derived values automatically recompute when dependencies change while maintaining optimal performance.
162
-
163
- ```tsx
164
- function FilteredContacts() {
165
- const [filter] = useContacts((state) => state.filter);
166
-
167
- // This selector recomputes when filter dependency changes
168
- const [filtered] = useContacts(
169
- (state) => state.items.filter((c) => c.name.includes(filter)),
170
- [filter], // Dependencies array
171
- );
172
-
173
- return <ContactList contacts={filtered} />;
174
- }
175
-
176
- // Or use options object for more control
177
- function FilteredContactsAdvanced() {
178
- const [filter] = useContacts((state) => state.filter);
179
-
180
- const [filtered] = useContacts((state) => state.items.filter((c) => c.name.includes(filter)), {
181
- dependencies: [filter],
182
- isEqualRoot: (a, b) => a.items === b.items,
183
- isEqual: (a, b) => a.length === b.length,
184
- });
185
-
186
- return <ContactList contacts={filtered} />;
187
- }
188
- ```
189
-
190
- ### 3️⃣ Reusable Selector Hooks
191
-
192
- Create derived hooks that can be reused across components and chained together.
193
-
194
- ```tsx
195
- // Create reusable selectors
196
- const useContactsArray = useContacts.createSelectorHook((state) => state.items);
197
-
198
- const useContactsCount = useContactsArray.createSelectorHook((items) => items.length);
199
-
200
- const useFilteredContacts = useContactsArray.createSelectorHook((items, filter) =>
201
- items.filter((c) => c.name.includes(filter)),
202
- );
203
-
204
- // Use them in components
205
- function Stats() {
206
- const [count] = useContactsCount();
207
- return <div>Total contacts: {count}</div>;
208
- }
209
-
210
- function ContactList() {
211
- const [filter] = useContacts((state) => state.filter);
212
-
213
- // Selector hooks still support inline selectors with dependencies!
214
- const [filtered] = useContactsArray(
215
- (contacts) => contacts.filter((c) => c.name.includes(filter)),
216
- [filter],
217
- );
218
-
219
- return <ul>{/* ... */}</ul>;
220
- }
221
- ```
222
-
223
- **✅ Important:** All selector hooks share the same state mutator, ensuring consistency:
224
-
225
- ```tsx
226
- const [, setContactsArray] = useContactsArray();
227
- const [, setContactsOriginal] = useContacts();
228
-
229
- // These are the same function!
230
- console.log(setContactsArray === setContactsOriginal); // true
231
- console.log(setContactsArray === useContacts.setState); // true
232
- ```
233
-
234
- ### 4️⃣ Actions - Structured State Mutations
235
-
236
- Restrict state modifications to predefined actions for better architecture and type safety.
237
-
238
- ```tsx
239
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
240
-
241
- const useContacts = createGlobalState(
242
- { items: [], filter: '', loading: false },
243
- {
244
- actions: {
245
- // Async actions are fully supported
246
- async fetch() {
247
- return async ({ setState, getState }) => {
248
- setState((prev) => ({ ...prev, loading: true }));
249
-
250
- try {
251
- const items = await fetchContacts();
252
- setState((prev) => ({ ...prev, items, loading: false }));
253
- } catch (error) {
254
- setState((prev) => ({ ...prev, loading: false }));
255
- }
256
- };
257
- },
258
-
259
- // Parameterized actions
260
- setFilter(filter: string) {
261
- return ({ setState }) => {
262
- setState((prev) => ({ ...prev, filter }));
263
- };
264
- },
265
-
266
- // Actions can access other actions
267
- addContact(contact: Contact) {
268
- return ({ setState, actions }) => {
269
- setState((prev) => ({
270
- ...prev,
271
- items: [...prev.items, contact],
272
- }));
273
-
274
- // Clear filter after adding
275
- actions.setFilter('');
276
- };
277
- },
278
- },
279
- },
280
- );
281
-
282
- // When actions are defined, the second element is the actions object
283
- function ContactManager() {
284
- const [state, actions] = useContacts();
285
-
286
- useEffect(() => {
287
- actions.fetch();
288
- }, []);
289
-
290
- return (
291
- <div>
292
- {state.loading && <Spinner />}
293
- <input value={state.filter} onChange={(e) => actions.setFilter(e.target.value)} />
294
- <button onClick={() => actions.addContact(newContact)}>Add Contact</button>
295
- </div>
296
- );
297
- }
298
- ```
299
-
300
- **Note:** When actions are defined, direct `setState` is replaced by the actions object. However, `setState` is still available via the API for testing: `useContacts.setState()`
301
-
302
- ---
303
-
304
- ## � Action Groups with `actionsFor`
305
-
306
- Need to **extend a store with additional actions** without modifying the original store definition? Use `actionsFor` to create action groups that can access both the store's state **and parent store actions**! Perfect for composition and code organization! 🎉
307
-
308
- ### 📌 Direct Binding
309
-
310
- Bind actions directly to a store:
311
-
312
- ```tsx
313
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
314
- import { actionsFor } from 'react-hooks-global-states/actionsFor';
315
-
316
- const useCounter = createGlobalState(0);
317
-
318
- const counterActions = actionsFor(useCounter.api(), {
319
- increment() {
320
- return ({ setState, getState }) => {
321
- setState(getState() + 1);
322
- };
323
- },
324
- decrement() {
325
- return ({ setState, getState }) => {
326
- setState(getState() - 1);
327
- };
328
- },
329
- reset() {
330
- return ({ setState }) => {
331
- setState(0);
332
- };
333
- },
334
- });
335
-
336
- // Use them anywhere in your app!
337
- counterActions.increment();
338
- counterActions.decrement();
339
- counterActions.reset();
340
- ```
341
-
342
- ### 📌 Builder Pattern (Reusable Templates!)
343
-
344
- Create **reusable action templates** that can be applied to multiple stores. This is incredibly powerful for code reuse! 💪
345
-
346
- ```tsx
347
- import { actionsFor } from 'react-hooks-global-states/actionsFor';
348
-
349
- // Create a reusable template
350
- const withCounterActions = actionsFor().with({
351
- increment() {
352
- return ({ setState, getState }) => {
353
- setState(getState() + 1);
354
- };
355
- },
356
- decrement() {
357
- return ({ setState, getState }) => {
358
- setState(getState() - 1);
359
- };
360
- },
361
- incrementBy(amount: number) {
362
- return ({ setState, getState }) => {
363
- setState(getState() + amount);
364
- };
365
- },
366
- });
367
-
368
- // Apply it to different stores! 🚀
369
- const counter1Actions = withCounterActions(useCounter1.api());
370
- const counter2Actions = withCounterActions(useCounter2.api());
371
-
372
- counter1Actions.increment(); // Updates counter1
373
- counter2Actions.incrementBy(5); // Updates counter2 by 5
374
- ```
375
-
376
- ### 📌 Accessing Parent Actions
377
-
378
- Actions created with `actionsFor` can call **parent store actions** via `this.actions`. This enables powerful composition patterns! 🔗
379
-
380
- ```tsx
381
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
382
- import { actionsFor } from 'react-hooks-global-states/actionsFor';
383
-
384
- const useCounter = createGlobalState(0, {
385
- actions: {
386
- reset() {
387
- return ({ setState }) => setState(0);
388
- },
389
- setTo(value: number) {
390
- return ({ setState }) => setState(value);
391
- },
392
- },
393
- });
394
-
395
- // Extend with additional actions that can use parent actions
396
- const extendedActions = actionsFor(useCounter.api(), {
397
- incrementAndLog() {
398
- return ({ setState, getState }) => {
399
- const newValue = getState() + 1;
400
- setState(newValue);
401
- console.log(`Incremented to ${newValue}`);
402
- };
403
- },
404
- resetWithMessage() {
405
- return function () {
406
- // Call parent action! ✨
407
- this.actions.reset();
408
- console.log('Counter has been reset!');
409
- };
410
- },
411
- doubleIt() {
412
- return function ({ getState }) {
413
- // Access parent actions and use them
414
- const current = getState();
415
- this.actions.setTo(current * 2);
416
- };
417
- },
418
- });
419
-
420
- extendedActions.resetWithMessage(); // Calls parent reset() and logs message
421
- extendedActions.doubleIt(); // Doubles the counter using parent setTo()
422
- ```
423
-
424
- ### 📌 Complex Example: Async Actions with Dependencies
425
-
426
- ```tsx
427
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
428
- import { actionsFor } from 'react-hooks-global-states/actionsFor';
429
-
430
- const useTodos = createGlobalState(
431
- { items: [], loading: false },
432
- {
433
- actions: {
434
- setLoading(loading: boolean) {
435
- return ({ setState, getState }) => {
436
- setState({ ...getState(), loading });
437
- };
438
- },
439
- },
440
- },
441
- );
442
-
443
- const todoActions = actionsFor(useTodos.api(), {
444
- async fetchTodos() {
445
- return async function ({ setState, getState }) {
446
- // Use parent action
447
- this.actions.setLoading(true);
448
-
449
- try {
450
- const items = await fetch('/api/todos').then((r) => r.json());
451
- setState({ ...getState(), items, loading: false });
452
- } catch (error) {
453
- this.actions.setLoading(false);
454
- throw error;
455
- }
456
- };
457
- },
458
-
459
- addTodo(text: string) {
460
- return ({ setState, getState }) => {
461
- const newTodo = { id: Date.now(), text, completed: false };
462
- const items = [...getState().items, newTodo];
463
- setState({ ...getState(), items });
464
- };
465
- },
466
-
467
- async addAndRefresh(text: string) {
468
- return async function () {
469
- // Compose multiple actions! 🎯
470
- this.addTodo(text);
471
- await this.fetchTodos();
472
- };
473
- },
474
- });
475
-
476
- // Use them!
477
- await todoActions.fetchTodos();
478
- todoActions.addTodo('Learn actionsFor');
479
- await todoActions.addAndRefresh('This is awesome!');
480
- ```
481
-
482
- ### ✨ Why Use `actionsFor`?
483
-
484
- - **🔄 Composability** – Mix and match action groups across stores without modifying original definitions
485
- - **📦 Reusability** – Define action templates once, use them everywhere
486
- - **🔗 Access Parent Actions** – New actions can leverage existing store logic via `this.actions`
487
- - **💪 Type Safety** – Full TypeScript inference for actions, parameters, and state
488
- - **🎯 Separation of Concerns** – Keep core store simple, extend with specialized action groups
489
- - **🚀 Progressive Enhancement** – Start simple, add complexity as needed
490
-
491
- ---
492
-
493
- ## �🌐 Non-Component Usage
494
-
495
- Access and manipulate state outside React components - perfect for services, utilities, and event handlers.
496
-
497
- ```tsx
498
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
499
-
500
- const useAuth = createGlobalState({ user: null, token: null });
501
-
502
- // API service
503
- class AuthService {
504
- async login(credentials: Credentials) {
505
- const { user, token } = await api.login(credentials);
506
-
507
- // Direct state access
508
- useAuth.setState({ user, token });
509
- }
510
-
511
- async logout() {
512
- await api.logout();
513
- useAuth.setState({ user: null, token: null });
514
- }
515
-
516
- getToken() {
517
- // Get current state synchronously
518
- return useAuth.getState().token;
519
- }
520
- }
521
-
522
- // Subscribe to changes outside components
523
- const unsubscribe = useAuth.subscribe((state) => {
524
- console.log('Auth state changed:', state);
525
-
526
- // Sync to external storage
527
- if (state.token) {
528
- localStorage.setItem('token', state.token);
529
- }
530
- });
531
-
532
- // Later: cleanup
533
- unsubscribe();
534
- ```
535
-
536
- ### Subscriptions with Selectors
537
-
538
- Subscriptions support the same selector API as hooks:
539
-
540
- ```tsx
541
- // Subscribe to a specific part of state
542
- const unsubscribe = useAuth.subscribe(
543
- (state) => state.user,
544
- (user) => {
545
- console.log('User changed:', user);
546
- },
547
- );
548
-
549
- // With options
550
- const unsubscribe2 = useAuth.subscribe(
551
- (state) => state.user,
552
- (user) => {
553
- analytics.identify(user);
554
- },
555
- {
556
- skipFirst: true, // Don't call on initial subscribe
557
- isEqual: (a, b) => a?.id === b?.id,
558
- },
559
- );
560
- ```
561
-
562
- ### Cross-State Dependencies
563
-
564
- One global state can react to changes in another:
565
-
566
- ```tsx
567
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
568
-
569
- const useSelectedContact = createGlobalState<Contact | null>(null, {
570
- callbacks: {
571
- onInit: ({ setState, getState }) => {
572
- // When contacts change, clear selection if contact was deleted
573
- return useContacts.subscribe(
574
- (state) => state.items,
575
- (contacts) => {
576
- const selected = getState();
577
- if (selected && !contacts.find((c) => c.id === selected.id)) {
578
- setState(null);
579
- }
580
- },
581
- );
582
- },
583
- },
584
- });
585
- ```
586
-
587
- ---
588
-
589
- ## 🎭 Context API - Scoped State
590
-
591
- When you need state scoped to a component tree instead of globally available. Same powerful API, different scope! 🎯
592
-
593
- ```tsx
594
- import { createContext } from 'react-hooks-global-states/createContext';
595
-
596
- // Create a context with initial state
597
- const TodoListContext = createContext(
598
- { todos: [], filter: 'all' },
599
- {
600
- actions: {
601
- addTodo(text: string) {
602
- return ({ setState }) => {
603
- const newTodo = { id: Date.now(), text, completed: false };
604
- setState((prev) => ({
605
- ...prev,
606
- todos: [...prev.todos, newTodo],
607
- }));
608
- };
609
- },
610
-
611
- toggleTodo(id: number) {
612
- return ({ setState }) => {
613
- setState((prev) => ({
614
- ...prev,
615
- todos: prev.todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)),
616
- }));
617
- };
618
- },
619
-
620
- setFilter(filter: 'all' | 'active' | 'completed') {
621
- return ({ setState }) => {
622
- setState((prev) => ({ ...prev, filter }));
623
- };
624
- },
625
- },
626
- },
627
- );
628
-
629
- // Wrap your component tree
630
- function App() {
631
- return (
632
- <TodoListContext.Provider>
633
- <TodoList />
634
- <TodoStats />
635
- </TodoListContext.Provider>
636
- );
637
- }
638
-
639
- // Use the context hook
640
- function TodoList() {
641
- const [state, actions] = TodoListContext.use();
642
-
643
- return (
644
- <div>
645
- <input
646
- onKeyPress={(e) => {
647
- if (e.key === 'Enter') {
648
- actions.addTodo(e.currentTarget.value);
649
- }
650
- }}
651
- />
652
-
653
- {state.todos.map((todo) => (
654
- <div key={todo.id} onClick={() => actions.toggleTodo(todo.id)}>
655
- {todo.text}
656
- </div>
657
- ))}
658
- </div>
659
- );
660
- }
661
-
662
- // Access context API without triggering re-renders
663
- function Logger() {
664
- const api = TodoListContext.use.api();
665
-
666
- useEffect(() => {
667
- // This component won't re-render when state changes
668
- const unsubscribe = api.subscribe((state) => {
669
- console.log('State changed:', state);
670
- });
671
-
672
- return unsubscribe;
673
- }, []);
674
-
675
- return null;
676
- }
677
- ```
678
-
679
- ### Context Features
680
-
681
- All the same features as global state:
682
-
683
- ```tsx
684
- // Selectors
685
- const [todos] = TodoListContext.use((state) => state.todos);
686
-
687
- // Selector hooks
688
- const useTodos = TodoListContext.use.createSelectorHook((state) => state.todos);
689
- const useTodoCount = useTodos.createSelectorHook((todos) => todos.length);
690
-
691
- // Observables
692
- const { context } = TodoListContext.Provider.makeProviderWrapper();
693
- const todoObservable = context.current.createObservable((state) => state.todos);
694
-
695
- // Access actions
696
- const actions = TodoListContext.use.actions();
697
- const [count, actions] = TodoListContext.use();
698
-
699
- // Non-reactive API access
700
- const api = TodoListContext.use.api();
701
- api.getState();
702
- api.setState({ todos: [], filter: 'all' });
703
- api.subscribe((state) => console.log(state));
704
- ```
705
-
706
- ### Multiple Providers
707
-
708
- Each provider creates an isolated state instance:
709
-
710
- ```tsx
711
- function App() {
712
- return (
713
- <div>
714
- <TodoListContext.Provider>
715
- <TodoList title="Work Tasks" />
716
- </TodoListContext.Provider>
717
-
718
- <TodoListContext.Provider>
719
- <TodoList title="Personal Tasks" />
720
- </TodoListContext.Provider>
721
- </div>
722
- );
723
- }
724
- ```
725
-
726
- ---
727
-
728
- ## 🔭 Observables - Reactive State Fragments
729
-
730
- Observables provide a subscription-based API for watching specific state changes, useful for non-component code. Think of them as lightweight reactive streams! 🌊
731
-
732
- ```tsx
733
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
734
-
735
- const useCounter = createGlobalState(0);
736
-
737
- // Create an observable that transforms the state
738
- const counterLogs = useCounter.createObservable((count) => `Counter is at ${count}`);
739
-
740
- // Subscribe to changes
741
- const unsubscribe = counterLogs((message) => {
742
- console.log(message); // "Counter is at 0", "Counter is at 1", etc.
743
- });
744
-
745
- // Observables can be chained
746
- const isEven = useCounter.createObservable((count) => count % 2 === 0);
747
- const evenMessage = isEven.createObservable((even) => (even ? 'Even number' : 'Odd number'));
748
-
749
- evenMessage((msg) => console.log(msg));
750
-
751
- // Cleanup
752
- unsubscribe();
753
- ```
754
-
755
- ### Observable API
756
-
757
- Observables expose the same readonly API as hooks:
758
-
759
- ```tsx
760
- const observable = useCounter.createObservable((count) => count * 2);
761
-
762
- // Get current value
763
- observable.getState(); // returns doubled count
764
-
765
- // Subscribe to changes
766
- const unsub = observable.subscribe((doubled) => {
767
- console.log('Doubled value:', doubled);
768
- });
769
-
770
- // Create derived observables
771
- const quadrupled = observable.createObservable((doubled) => doubled * 2);
772
-
773
- // Create selector hooks from observables
774
- const useDoubled = observable.createSelectorHook((state) => state);
775
-
776
- // Cleanup
777
- observable.dispose(); // Cleans up subscriptions
778
- ```
779
-
780
- ---
781
-
782
- ## 🔄 Lifecycle Callbacks
783
-
784
- Hook into state initialization and changes for setup, cleanup, and validation. Perfect for side effects! ⚡
785
-
786
- ```tsx
787
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
788
-
789
- const useData = createGlobalState(
790
- { value: 0, lastUpdated: null },
791
- {
792
- callbacks: {
793
- // Called once when store initializes
794
- onInit: ({ setState, subscribe }) => {
795
- console.log('Store initialized');
796
-
797
- // Return cleanup function
798
- return () => {
799
- console.log('Store cleaned up');
800
- };
801
- },
802
-
803
- // Called after every state change
804
- onStateChanged: ({ state, previousState, identifier }) => {
805
- console.log('State changed:', previousState, '→', state);
806
-
807
- if (identifier) {
808
- console.log('Change identifier:', identifier);
809
- }
810
-
811
- // Update timestamp
812
- setState((prev) => ({ ...prev, lastUpdated: Date.now() }));
813
- },
814
-
815
- // Prevent specific state changes
816
- computePreventStateChange: ({ state, previousState }) => {
817
- // Prevent setting value to same number
818
- return state.value === previousState.value;
819
- },
820
-
821
- // Called when a component subscribes
822
- onSubscribed: (storeTools, subscription) => {
823
- console.log('New subscriber added');
824
- },
825
- },
826
- },
827
- );
828
-
829
- // Trigger with identifier for debugging
830
- useData.setState({ value: 5 }, { identifier: 'manual-update' });
831
-
832
- // Force update even if state is the same
833
- useData.setState((state) => state, { forceUpdate: true });
834
- ```
835
-
836
- ---
837
-
838
- ## 📊 Metadata - Non-Reactive Data
839
-
840
- Store non-reactive information alongside your state without triggering re-renders. Great for tracking stats, timestamps, and other auxiliary data! 📈
841
-
842
- ```tsx
843
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
844
-
845
- const useCounter = createGlobalState(0, {
846
- metadata: {
847
- createdAt: Date.now(),
848
- updateCount: 0,
849
- },
850
- });
851
-
852
- function Counter() {
853
- const [count, setCount, metadata] = useCounter();
854
-
855
- // Metadata doesn't cause re-renders when modified
856
- metadata.updateCount += 1;
857
-
858
- return (
859
- <div>
860
- <p>Count: {count}</p>
861
- <p>Updates: {metadata.updateCount}</p>
862
- <button onClick={() => setCount(count + 1)}>Increment</button>
863
- </div>
864
- );
865
- }
866
-
867
- // Access metadata externally
868
- const meta = useCounter.getMetadata();
869
- console.log(meta.createdAt);
870
-
871
- // Update metadata
872
- useCounter.setMetadata((prev) => ({
873
- ...prev,
874
- lastAccessed: Date.now(),
875
- }));
876
- ```
877
-
878
- ---
879
-
880
- ## 🧪 Testing & Debugging
881
-
882
- ### Reset State for Tests
883
-
884
- ```tsx
885
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
886
-
887
- const useCounter = createGlobalState(0);
888
-
889
- afterEach(() => {
890
- // Reset to initial state
891
- useCounter.reset(0, {});
892
- });
893
-
894
- test('counter increments', () => {
895
- const { result } = renderHook(() => useCounter());
896
- const [, setCount] = result.current;
897
-
898
- act(() => setCount(1));
899
-
900
- expect(result.current[0]).toBe(1);
901
- });
902
- ```
903
-
904
- ### Dispose for Cleanup
905
-
906
- ```tsx
907
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
908
-
909
- const useTemp = createGlobalState({ data: [] });
910
-
911
- // When done with the store
912
- useTemp.dispose(); // Clears all subscriptions and executes cleanup
913
- ```
914
-
915
- ### DevTools Extension
916
-
917
- **[Install React Hooks Global States DevTools](https://chromewebstore.google.com/detail/bafojplmkpejhglhjpibpdhoblickpee)**
918
-
919
- Features:
920
-
921
- - 📊 Track all state changes in real-time
922
- - ✏️ Modify state directly from the extension
923
- - ⏮️ Time-travel debugging - restore previous states
924
- - 🎯 Action granularity - see individual action executions
925
- - 🔍 Inspect metadata and subscribers
926
-
927
- ### Debug Identifiers
928
-
929
- ```tsx
930
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
931
-
932
- // Name your stores for easier debugging
933
- const useAuth = createGlobalState({ user: null }, { name: 'AuthStore' });
934
-
935
- // Use identifiers when updating state
936
- useAuth.setState(
937
- { user: newUser },
938
- {
939
- identifier: 'login-success',
940
- },
941
- );
942
- ```
943
-
944
- ### Type Inference Helpers
945
-
946
- ```tsx
947
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
948
- import type { InferAPI } from 'react-hooks-global-states/types';
949
-
950
- const useStore = createGlobalState(/* ... */);
951
-
952
- // Infer the complete API type
953
- type StoreAPI = InferAPI<typeof useStore>;
954
-
955
- // Use in functions that need the store tools
956
- function doSomething(store: StoreAPI) {
957
- store.getState();
958
- store.setState(/* ... */);
959
- store.subscribe(/* ... */);
960
- }
961
- ```
962
-
963
- ---
964
-
965
- ## 📚 API Reference
966
-
967
- ### `createGlobalState(initialState, options?)`
968
-
969
- Creates a global state hook.
970
-
971
- **Parameters:**
972
-
973
- - `initialState`: Initial state value
974
- - `options?`: Configuration object
975
- - `name?`: Debug name
976
- - `metadata?`: Non-reactive metadata
977
- - `callbacks?`: Lifecycle callbacks
978
- - `onInit?`: Called on initialization
979
- - `onStateChanged?`: Called after state changes
980
- - `computePreventStateChange?`: Validation function
981
- - `onSubscribed?`: Called when component subscribes
982
- - `actions?`: Action definitions
983
-
984
- **Returns:** State hook with extended API
985
-
986
- **Hook API:**
987
-
988
- - `use()` - Use in component (same as calling the hook directly)
989
- - `select(selector, deps?)` - Select a value without subscribing to state changes
990
- - `getState()` - Get current state
991
- - `setState(update, options?)` - Update state
992
- - `subscribe(callback)` / `subscribe(selector, callback, options?)` - Subscribe to changes
993
- - `getMetadata()` - Get metadata
994
- - `setMetadata(update)` - Update metadata
995
- - `createSelectorHook(selector, options?)` - Create derived hook
996
- - `createObservable(selector, options?)` - Create observable
997
- - `reset(state, metadata)` - Reset store
998
- - `dispose()` - Cleanup
999
- - `actions` - Action methods (when actions defined)
1000
- - `subscribers` - Active subscriptions (debugging)
1001
-
1002
- ### `createContext(initialState, options?)`
1003
-
1004
- Creates a context with the same API as global state, but scoped to a Provider.
1005
-
1006
- **Parameters:** Same as `createGlobalState`
1007
-
1008
- **Returns:** Context object
1009
-
1010
- - `Provider` - Context provider component
1011
- - `makeProviderWrapper()` - Get provider wrapper and context ref
1012
- - `use` - Use the context (must be inside Provider)
1013
- - Same API as global state hook
1014
- - `use.api()` - Get non-reactive API access
1015
- - `use.actions()` - Get just the actions
1016
-
1017
- ---
1018
-
1019
- ## 💡 Best Practices
1020
-
1021
- ### 1. Organize State by Domain
1022
-
1023
- ```tsx
1024
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
1025
-
1026
- // ✅ Good - domain-focused
1027
- const useAuth = createGlobalState({ ... });
1028
- const useCart = createGlobalState({ ... });
1029
- const useProducts = createGlobalState({ ... });
1030
-
1031
- // ❌ Avoid - one giant state
1032
- const useAppState = createGlobalState({
1033
- auth: { ... },
1034
- cart: { ... },
1035
- products: { ... },
1036
- });
1037
- ```
1038
-
1039
- ### 2. Use Actions for Complex Logic
1040
-
1041
- ```tsx
1042
- import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
1043
-
1044
- // ✅ Good - encapsulated business logic
1045
- const useCart = createGlobalState(initialCart, {
1046
- actions: {
1047
- addItem(product: Product, quantity: number) {
1048
- return ({ setState }) => {
1049
- // Validation, calculations, side effects
1050
- if (quantity <= 0) return;
1051
-
1052
- setState((cart) => ({
1053
- ...cart,
1054
- items: [...cart.items, { product, quantity }],
1055
- total: calculateTotal(cart, product, quantity),
1056
- }));
1057
- };
1058
- },
1059
- },
1060
- });
1061
-
1062
- // ❌ Avoid - business logic in components
1063
- function Component() {
1064
- const [cart, setCart] = useCart();
1065
-
1066
- const addItem = (product, quantity) => {
1067
- if (quantity <= 0) return;
1068
- setCart((cart) => ({
1069
- ...cart,
1070
- items: [...cart.items, { product, quantity }],
1071
- total: calculateTotal(cart, product, quantity),
1072
- }));
1073
- };
1074
- }
1075
- ```
1076
-
1077
- ### 3. Use Selectors for Performance
1078
-
1079
- ```tsx
1080
- // ✅ Good - only re-renders when relevant data changes
1081
- const [userName] = useAuth((state) => state.user?.name);
1082
-
1083
- // ❌ Avoid - re-renders on any auth state change
1084
- const [auth] = useAuth();
1085
- const userName = auth.user?.name;
1086
- ```
1087
-
1088
- ### 4. Leverage Selector Hooks for Reusability
1089
-
1090
- ```tsx
1091
- // ✅ Good - reusable, testable
1092
- const useUserName = useAuth.createSelectorHook((state) => state.user?.name);
1093
-
1094
- // Use in multiple components
1095
- function Header() {
1096
- const [name] = useUserName();
1097
- return <div>Hello {name}</div>;
1098
- }
1099
-
1100
- function Profile() {
1101
- const [name] = useUserName();
1102
- return <h1>{name}'s Profile</h1>;
1103
- }
1104
- ```
1105
-
1106
- ### 5. Use Context for Isolated Features
1107
-
1108
- ```tsx
1109
- import { createContext } from 'react-hooks-global-states/createContext';
1110
-
1111
- // ✅ Good - wizard state isolated to flow
1112
- const WizardContext = createContext({ step: 1, data: {} });
1113
-
1114
- function CheckoutWizard() {
1115
- return (
1116
- <WizardContext.Provider>
1117
- <WizardSteps />
1118
- </WizardContext.Provider>
1119
- );
1120
- }
1121
-
1122
- // ❌ Avoid - global state for temporary UI state
1123
- const useWizardState = createGlobalState({ step: 1, data: {} });
1124
- ```
1125
-
1126
- ### 6. Use `actionsFor` for Extending Stores
1127
-
1128
- ```tsx
1129
- import { actionsFor } from 'react-hooks-global-states/actionsFor';
1130
-
1131
- // ✅ Good - extend stores with reusable action templates
1132
- const withLogging = actionsFor().with({
1133
- logState() {
1134
- return ({ getState }) => {
1135
- console.log('Current state:', getState());
1136
- };
1137
- },
1138
- });
1139
-
1140
- // Apply to any store!
1141
- const counterWithLogging = withLogging(useCounter.api());
1142
- const authWithLogging = withLogging(useAuth.api());
1143
-
1144
- // ❌ Avoid - repeating the same actions in multiple stores
1145
- const useCounter = createGlobalState(0, {
1146
- actions: {
1147
- logState() {
1148
- /* duplicate code */
1149
- },
1150
- },
1151
- });
1152
-
1153
- const useAuth = createGlobalState(null, {
1154
- actions: {
1155
- logState() {
1156
- /* duplicate code */
1157
- },
1158
- },
1159
- });
1160
- ```
1161
-
1162
- ---
1163
-
1164
- ## 🔗 Resources
1165
-
1166
- - **[Live Example](https://johnny-quesada-developer.github.io/global-hooks-example/)**
1167
- - **[Video Overview](https://www.youtube.com/watch?v=1UBqXk2MH8I/)**
1168
- - **[CodePen Demo](https://codepen.io/johnnynabetes/pen/WNmeGwb?editors=0010)**
1169
- - **[DevTools Extension](https://chromewebstore.google.com/detail/bafojplmkpejhglhjpibpdhoblickpee)**
1170
- - **[GitHub Repository](https://github.com/johnny-quesada-developer/react-hooks-global-states)**
1171
-
1172
- ## 📦 Related Packages
1173
-
1174
- - **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** - Web with localStorage integration
1175
- - **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** - React Native with AsyncStorage integration
1176
-
1177
- ---
1178
-
1179
- ## 📄 License
1180
-
1181
- MIT © [Johnny Quesada](https://github.com/johnny-quesada-developer)
1182
-
1183
- ---
1184
-
1185
- ## 🙏 Contributing
1186
-
1187
- Contributions are welcome! Please feel free to submit a Pull Request.
1188
-
1189
- ---
1190
-
1191
- **Made with ❤️ by developers, for developers**