react-native-global-state-hooks 15.0.1 → 15.0.3

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
@@ -1,400 +1,1407 @@
1
1
  # react-native-global-state-hooks 🌟
2
2
 
3
- ![Image John Avatar](https://raw.githubusercontent.com/johnny-quesada-developer/global-hooks-example/main/public/avatar2.jpeg)
3
+ <div align="center">
4
4
 
5
- Effortless **global state management** for React & React Native! 🚀 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
+ ![Johnny Quesada](https://raw.githubusercontent.com/johnny-quesada-developer/global-hooks-example/main/public/avatar2.jpeg)
6
6
 
7
- ---
7
+ </div>
8
+
9
+ <div align="center">
10
+
11
+ **Zero setup. Native persistence. Maximum performance.** 🚀
12
+
13
+ _The simplicity of `useState`, shared across your React Native app._ ✨
8
14
 
9
- ## 🔗 Explore More
15
+ [![npm version](https://img.shields.io/npm/v/react-native-global-state-hooks.svg)](https://www.npmjs.com/package/react-native-global-state-hooks)
16
+ [![Downloads](https://img.shields.io/npm/dm/react-native-global-state-hooks.svg)](https://www.npmjs.com/package/react-native-global-state-hooks)
17
+ [![License](https://img.shields.io/npm/l/react-native-global-state-hooks.svg)](https://github.com/johnny-quesada-developer/react-native-global-state-hooks/blob/master/LICENSE)
10
18
 
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**).
19
+ [**NPM**](https://www.npmjs.com/package/react-native-global-state-hooks) • [**GitHub**](https://github.com/johnny-quesada-developer/react-native-global-state-hooks) • [**Core API Demo**](https://johnny-quesada-developer.github.io/global-hooks-example/) • [**Video Tutorial**](https://www.youtube.com/watch?v=1UBqXk2MH8I/)
20
+
21
+ </div>
16
22
 
17
23
  ---
18
24
 
19
- ## 🚀 React Hooks Global States - DevTools Extension
25
+ ## 🎯 The One-Liner
20
26
 
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.
27
+ ```tsx
28
+ import { createGlobalState } from "react-native-global-state-hooks";
22
29
 
23
- ### 🔗 [Install the DevTools Extension for Chrome](https://chromewebstore.google.com/detail/bafojplmkpejhglhjpibpdhoblickpee/preview?hl=en&authuser=0)
30
+ export const useCounter = createGlobalState(0);
31
+ ```
32
+
33
+ **That's it.** No providers. No Redux boilerplate. No configuration files. Just shared state with a familiar React API. 🎨
34
+
35
+ ```tsx
36
+ import { Button } from "react-native";
24
37
 
25
- ### 📸 DevTools Highlights
38
+ function Counter() {
39
+ const [count, setCount] = useCounter();
26
40
 
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. |
41
+ return <Button title={`Count: ${count}`} onPress={() => setCount((current) => current + 1)} />;
42
+ }
43
+ ```
31
44
 
32
45
  ---
33
46
 
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. |
47
+ ## 🚀 Why Developers Love This Library
38
48
 
39
- <br>
49
+ ### 🎓 **Zero Learning Curve**
40
50
 
41
- ## 🗂️ Async Persist Storage
51
+ If you know `useState`, the basic API already feels familiar:
42
52
 
43
- To persist the global state using **Async Storage**, simply add the `asyncStorage` option:
53
+ ```tsx
54
+ // Local state
55
+ const [count, setCount] = useState(0);
44
56
 
45
- ```ts
46
- const useCount = createGlobalState(0, {
47
- asyncStorage: {
48
- key: "count",
49
- },
50
- });
57
+ // Global state
58
+ const [count, setCount] = useCounter();
51
59
  ```
52
60
 
53
- ### 🔹 How It Works
54
-
55
- ✅ **Automatically syncs the state with Async Storage** if the value is serializable.
56
- ✅ **Provides an `isAsyncStorageReady` flag** to indicate when the async storage has been reviewed and committed.
57
- ✅ **Uses `@react-native-async-storage/async-storage` by default** (make sure to install this package if needed).
58
- ✅ **Allows custom async storage managers** with `asyncStorageWrapper.addAsyncStorageManager(customAsyncStorageManager)`;
61
+ ### **Surgical Re-renders**
59
62
 
60
- Inside your components:
63
+ Subscribe to only the part of the state your component needs.
61
64
 
62
65
  ```tsx
63
- const [count, setCount, { isAsyncStorageReady }] = useCount();
66
+ const [name] = useStore((state) => state.user.name);
64
67
  ```
65
68
 
66
- If you specify a key in `asyncStorage`, the state value persists automatically when serializable. When connecting to async storage, expect a **second render** that updates `isAsyncStorageReady`, indicating that the storage has been reviewed and the state is committed.
69
+ The component only needs to react when its selected value changes.
67
70
 
68
- ### 🔧 Custom Async Storage Manager
71
+ ### 🔗 **Chainable Selectors**
69
72
 
70
- You can configure your own storage selection by using `asyncStorageWrapper.addAsyncStorageManager`. Ensure that the manager is **added before any hook is called**.
73
+ Build reusable state hooks from other selector hooks.
71
74
 
72
- `index.ts`
75
+ ```tsx
76
+ const useUsers = useStore.createSelectorHook((state) => state.users);
73
77
 
74
- ```ts
75
- import { asyncStorageWrapper } from "react-global-state-hooks";
76
- asyncStorageWrapper.addAsyncStorageManager(customAsyncStorageManager);
78
+ const useAdmins = useUsers.createSelectorHook((users) => users.filter((user) => user.role === "admin"));
77
79
  ```
78
80
 
79
- ## 🛠 Creating a Global State
81
+ ### 🎭 **Actions (Optional)**
80
82
 
81
- Define a **global state** in **one line**:
83
+ Keep mutation logic close to the store when you want more structure.
82
84
 
83
85
  ```tsx
84
- import { createGlobalState } from "react-hooks-global-states/createGlobalState";
85
- export const useCount = createGlobalState(0);
86
+ const useCounter = createGlobalState(0, {
87
+ actions: {
88
+ increment(amount = 1) {
89
+ return ({ setState, getState }) => {
90
+ setState(getState() + amount);
91
+ };
92
+ },
93
+ },
94
+ });
86
95
  ```
87
96
 
88
- Now, use it inside a component:
97
+ ### 🎪 **Context Mode**
98
+
99
+ Need isolated state instead of app-wide state? Use the same state model inside a Provider.
89
100
 
90
101
  ```tsx
91
- const [count, setCount] = useCount();
92
- return <Button onClick={() => setCount((count) => count + 1)}>{count}</Button>;
102
+ const Form = createContext({
103
+ name: "",
104
+ email: "",
105
+ });
106
+
107
+ <Form.Provider>
108
+ <FormFields />
109
+ </Form.Provider>;
93
110
  ```
94
111
 
95
- Works just like **useState**, but the **state is shared globally**! 🎉
112
+ ### 📱 **Native Async Persistence**
96
113
 
97
- ---
114
+ Persist state with React Native async storage semantics.
98
115
 
99
- ## 🎯 Selectors: Subscribing to Specific State Changes
116
+ ```tsx
117
+ const useSettings = createGlobalState(
118
+ { theme: "dark" as "dark" | "light" },
119
+ {
120
+ asyncStorage: {
121
+ key: "settings",
122
+ validator: ({ restored, initial }) => {
123
+ if (!validateStateIntegrity(restored)) return initial;
124
+ return restored;
125
+ },
126
+ },
127
+ },
128
+ );
129
+ ```
130
+
131
+ ### 📦 **Non-Reactive API**
100
132
 
101
- For **complex state objects**, you can subscribe to specific properties instead of the entire state:
133
+ Read, update, or subscribe to state outside React components.
102
134
 
103
135
  ```tsx
104
- export const useContacts = createGlobalState({ entities: [], selected: new Set<number>() });
136
+ const currentUser = useAuth.getState().user;
137
+
138
+ useAuth.setState((state) => ({
139
+ ...state,
140
+ user: nextUser,
141
+ }));
105
142
  ```
106
143
 
107
- To access only the `entities` property:
144
+ ---
108
145
 
109
- ```tsx
110
- const [contacts] = useContacts((state) => state.entities);
111
- return (
112
- <ul>
113
- {contacts.map((contact) => (
114
- <li key={contact.id}>{contact.name}</li>
115
- ))}
116
- </ul>
117
- );
146
+ ## 📦 Installation
147
+
148
+ ```bash
149
+ npm install react-native-global-state-hooks
118
150
  ```
119
151
 
120
- ### 📌 Using Dependencies in Selectors
152
+ or
153
+
154
+ ```bash
155
+ yarn add react-native-global-state-hooks
156
+ ```
157
+
158
+ ### 💾 Persisted State
159
+
160
+ The core state library does **not** require AsyncStorage.
161
+
162
+ But if you want the built-in React Native persistence backend, also install:
163
+
164
+ ```bash
165
+ npm install @react-native-async-storage/async-storage
166
+ ```
167
+
168
+ or
169
+
170
+ ```bash
171
+ yarn add @react-native-async-storage/async-storage
172
+ ```
173
+
174
+ `@react-native-async-storage/async-storage` is an **optional peer dependency**. You can also provide your own async storage manager or a per-store persistence adapter.
175
+
176
+ ---
121
177
 
122
- 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:
178
+ ## 🎬 Quick Start
179
+
180
+ ### 30 Seconds to Global State
123
181
 
124
182
  ```tsx
125
- const [contacts] = useContacts(
126
- (state) => state.entities.filter((item) => item.name.includes(filter)),
127
- [filter],
128
- );
183
+ import { Button, Text, View } from "react-native";
184
+ import { createGlobalState } from "react-native-global-state-hooks";
185
+
186
+ const useTheme = createGlobalState("dark" as "light" | "dark");
187
+
188
+ function ThemeToggle() {
189
+ const [theme, setTheme] = useTheme();
190
+
191
+ return (
192
+ <View>
193
+ <Text>{theme} mode</Text>
194
+
195
+ <Button
196
+ title="Toggle theme"
197
+ onPress={() => {
198
+ setTheme((current) => (current === "dark" ? "light" : "dark"));
199
+ }}
200
+ />
201
+ </View>
202
+ );
203
+ }
129
204
  ```
130
205
 
131
- Alternatively, you can pass dependencies inside an **options object**:
206
+ ### 60 Seconds to Production-Ready
207
+
208
+ A persisted React Native store initializes **asynchronously**, so the hook also exposes storage readiness through metadata.
132
209
 
133
210
  ```tsx
134
- const [contacts] = useContacts((state) => state.entities.filter((item) => item.name.includes(filter)), {
135
- dependencies: [filter],
136
- isEqualRoot: (a, b) => a.entities === b.entities,
211
+ import { ActivityIndicator, Switch, Text, View } from "react-native";
212
+ import { createGlobalState } from "react-native-global-state-hooks";
213
+
214
+ type Settings = {
215
+ darkMode: boolean;
216
+ haptics: boolean;
217
+ };
218
+
219
+ const initialSettings: Settings = {
220
+ darkMode: true,
221
+ haptics: true,
222
+ };
223
+
224
+ const useSettings = createGlobalState(initialSettings, {
225
+ asyncStorage: {
226
+ key: "app-settings",
227
+
228
+ validator: ({ restored, initial }) => {
229
+ if (!validateStateIntegrity(restored)) return initial; // you could also throw an exception and the store will use the initial state
230
+ return settings as Settings;
231
+ },
232
+ },
233
+
234
+ actions: {
235
+ setDarkMode(enabled: boolean) {
236
+ return ({ setState }) => {
237
+ setState((state) => ({
238
+ ...state,
239
+ darkMode: enabled,
240
+ }));
241
+ };
242
+ },
243
+
244
+ setHaptics(enabled: boolean) {
245
+ return ({ setState }) => {
246
+ setState((state) => ({
247
+ ...state,
248
+ haptics: enabled,
249
+ }));
250
+ };
251
+ },
252
+ },
137
253
  });
254
+
255
+ function SettingsScreen() {
256
+ const [settings, actions, { isAsyncStorageReady }] = useSettings();
257
+
258
+ if (!isAsyncStorageReady) {
259
+ return <ActivityIndicator />;
260
+ }
261
+
262
+ return (
263
+ <View>
264
+ <Text>Dark mode</Text>
265
+ <Switch value={settings.darkMode} onValueChange={actions.setDarkMode} />
266
+
267
+ <Text>Haptics</Text>
268
+ <Switch value={settings.haptics} onValueChange={actions.setHaptics} />
269
+ </View>
270
+ );
271
+ }
138
272
  ```
139
273
 
140
- Unlike Redux, where only **root state changes trigger re-selection**, this approach ensures that **derived values recompute when dependencies change** while maintaining performance.
274
+ Now you have:
275
+
276
+ - ✅ Global React Native state
277
+ - ✅ Fine-grained selectors
278
+ - ✅ Type-safe actions
279
+ - ✅ Async persistence
280
+ - ✅ Restore readiness
281
+ - ✅ Validation and error handling
141
282
 
142
283
  ---
143
284
 
144
- ## 🔄 Reusing Selectors
285
+ ## 🌟 Core Features Deep Dive
286
+
287
+ ### 1️⃣ Global State with `createGlobalState`
288
+
289
+ Create state that lives outside the component tree and can be consumed anywhere in your React Native app.
145
290
 
146
- ### 📌 Creating a Selector
291
+ #### 🎨 The Basics
147
292
 
148
293
  ```tsx
149
- export const useContactsArray = useContacts.createSelectorHook((state) => state.entities);
150
- export const useContactsCount = useContactsArray.createSelectorHook((entities) => entities.length);
294
+ import { createGlobalState } from "react-native-global-state-hooks";
295
+
296
+ // Primitives
297
+ const useCount = createGlobalState(0);
298
+ const useTheme = createGlobalState("light" as "light" | "dark");
299
+ const useIsOnline = createGlobalState(true);
300
+
301
+ // Objects
302
+ const useUser = createGlobalState({
303
+ name: "Guest",
304
+ role: "viewer",
305
+ });
306
+
307
+ // Arrays
308
+ const useTodos = createGlobalState([
309
+ { id: 1, text: "Learn the library", done: true },
310
+ { id: 2, text: "Build something", done: false },
311
+ ]);
151
312
  ```
152
313
 
153
- ### 📌 Using Selectors in Components
314
+ #### 🎯 Surgical Re-renders with Selectors
154
315
 
155
316
  ```tsx
156
- const [contacts] = useContactsArray();
157
- const [count] = useContactsCount();
317
+ const useStore = createGlobalState({
318
+ user: {
319
+ name: "Johnny",
320
+ email: "johnny@example.com",
321
+ },
322
+ theme: "dark",
323
+ notifications: [],
324
+ settings: {
325
+ sound: true,
326
+ haptics: true,
327
+ },
328
+ });
329
+
330
+ function UserName() {
331
+ const [name] = useStore((state) => state.user.name);
332
+
333
+ return <Text>{name}</Text>;
334
+ }
335
+
336
+ function ThemeLabel() {
337
+ const theme = useStore.select((state) => state.theme);
338
+
339
+ return <Text>{theme}</Text>;
340
+ }
341
+
342
+ function NotificationCount() {
343
+ const [notifications] = useStore((state) => state.notifications);
344
+
345
+ return <Text>{notifications.length}</Text>;
346
+ }
158
347
  ```
159
348
 
160
- #### Selectors support inline selectors and dependencies
349
+ Each component subscribes to the value it actually uses instead of blindly reacting to the whole state object.
161
350
 
162
- You can still **use dependencies** inside a selector hook:
351
+ #### Computed Values with Dependencies
352
+
353
+ Selectors can also depend on component-local values.
163
354
 
164
355
  ```tsx
165
- const [filteredContacts] = useContactsArray(
166
- (contacts) => contacts.filter((c) => c.name.includes(filter)),
167
- [filter],
356
+ const useTodos = createGlobalState({
357
+ todos: [
358
+ { id: 1, text: "Task 1", completed: false, priority: "high" },
359
+ { id: 2, text: "Task 2", completed: true, priority: "low" },
360
+ { id: 3, text: "Task 3", completed: false, priority: "high" },
361
+ ],
362
+ });
363
+
364
+ function TodoList() {
365
+ const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
366
+
367
+ const [todos] = useTodos(
368
+ (state) => {
369
+ if (filter === "active") {
370
+ return state.todos.filter((todo) => !todo.completed);
371
+ }
372
+
373
+ if (filter === "completed") {
374
+ return state.todos.filter((todo) => todo.completed);
375
+ }
376
+
377
+ return state.todos;
378
+ },
379
+ [filter],
380
+ );
381
+
382
+ return (
383
+ <View>
384
+ {todos.map((todo) => (
385
+ <Text key={todo.id}>{todo.text}</Text>
386
+ ))}
387
+ </View>
388
+ );
389
+ }
390
+ ```
391
+
392
+ For more control, pass an options object:
393
+
394
+ ```tsx
395
+ const [todos] = useTodos(
396
+ (state) =>
397
+ state.todos.filter((todo) => (filter === "all" ? true : todo.completed === (filter === "completed"))),
398
+ {
399
+ dependencies: [filter],
400
+ isEqualRoot: (previous, next) => previous.todos === next.todos,
401
+ },
168
402
  );
169
403
  ```
170
404
 
171
- #### Selector hooks share the same state mutator
405
+ #### 🔗 Reusable Selector Hooks
172
406
 
173
- The **stateMutator remains the same** across all derived selectors, meaning actions and setState functions stay consistent.
407
+ Create hooks from hooks and compose them.
174
408
 
175
409
  ```tsx
176
- const [actions1] = useContactsArray();
177
- const [actions2] = useContactsCount();
410
+ const useStore = createGlobalState({
411
+ users: [
412
+ { id: 1, name: "Alice", role: "admin", active: true },
413
+ { id: 2, name: "Bob", role: "user", active: true },
414
+ { id: 3, name: "Charlie", role: "admin", active: false },
415
+ ],
416
+ });
178
417
 
179
- console.log(actions1 === actions2); // true
180
- ```
418
+ const useUsers = useStore.createSelectorHook((state) => state.users);
181
419
 
182
- ---
420
+ const useActiveUsers = useUsers.createSelectorHook((users) => users.filter((user) => user.active));
421
+
422
+ const useActiveAdmins = useActiveUsers.createSelectorHook((users) =>
423
+ users.filter((user) => user.role === "admin"),
424
+ );
183
425
 
184
- ## 🎛 State Actions: Controlling State Modifications
426
+ function UserStats() {
427
+ const [users] = useUsers();
428
+ const [activeUsers] = useActiveUsers();
429
+ const [activeAdmins] = useActiveAdmins();
430
+
431
+ return (
432
+ <View>
433
+ <Text>Total: {users.length}</Text>
434
+ <Text>Active: {activeUsers.length}</Text>
435
+ <Text>Active admins: {activeAdmins.length}</Text>
436
+ </View>
437
+ );
438
+ }
439
+ ```
185
440
 
186
- Restrict **state modifications** by defining custom actions:
441
+ #### 🎬 Actions When You Need Structure
442
+
443
+ Actions are optional. Add them when you want a defined mutation API.
187
444
 
188
445
  ```tsx
189
- export const useContacts = createGlobalState(
190
- { filter: "", items: [] },
446
+ type Todo = {
447
+ id: string;
448
+ text: string;
449
+ completed: boolean;
450
+ };
451
+
452
+ const useTodos = createGlobalState(
453
+ {
454
+ todos: [] as Todo[],
455
+ filter: "all" as "all" | "active" | "completed",
456
+ },
191
457
  {
192
458
  actions: {
193
- async fetch() {
194
- return async ({ setState }) => {
195
- const items = await fetchItems();
196
- setState({ items });
459
+ addTodo(text: string) {
460
+ return ({ setState }) => {
461
+ const todo: Todo = {
462
+ id: `${Date.now()}`,
463
+ text,
464
+ completed: false,
465
+ };
466
+
467
+ setState((state) => ({
468
+ ...state,
469
+ todos: [...state.todos, todo],
470
+ }));
197
471
  };
198
472
  },
199
- setFilter(filter: string) {
473
+
474
+ toggleTodo(id: string) {
200
475
  return ({ setState }) => {
201
- setState((state) => ({ ...state, filter }));
476
+ setState((state) => ({
477
+ ...state,
478
+ todos: state.todos.map((todo) =>
479
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo,
480
+ ),
481
+ }));
202
482
  };
203
483
  },
484
+
485
+ setFilter(filter: "all" | "active" | "completed") {
486
+ return ({ setState }) => {
487
+ setState((state) => ({
488
+ ...state,
489
+ filter,
490
+ }));
491
+ };
492
+ },
493
+ },
494
+ },
495
+ );
496
+
497
+ function TodoActions() {
498
+ const [, actions] = useTodos();
499
+
500
+ return <Button title="Add todo" onPress={() => actions.addTodo("New task")} />;
501
+ }
502
+ ```
503
+
504
+ #### 🔌 Non-Reactive API — Use Outside React
505
+
506
+ The store hook also exposes its API directly.
507
+
508
+ ```tsx
509
+ const useAuth = createGlobalState({
510
+ user: null as null | { id: string; name: string },
511
+ token: null as string | null,
512
+ });
513
+ ```
514
+
515
+ Read state outside a component:
516
+
517
+ ```tsx
518
+ const auth = useAuth.getState();
519
+ ```
520
+
521
+ Update it:
522
+
523
+ ```tsx
524
+ useAuth.setState((state) => ({
525
+ ...state,
526
+ user: {
527
+ id: "42",
528
+ name: "Johnny",
529
+ },
530
+ }));
531
+ ```
532
+
533
+ Subscribe to a state fragment:
534
+
535
+ ```tsx
536
+ const unsubscribe = useAuth.subscribe(
537
+ (state) => state.user,
538
+ (user) => {
539
+ console.log("User changed:", user);
540
+ },
541
+ );
542
+
543
+ // Later
544
+ unsubscribe();
545
+ ```
546
+
547
+ This is useful for networking layers, app lifecycle handlers, push-notification handlers, analytics, and other code that does not render UI.
548
+
549
+ #### 🔭 Observable Fragments
550
+
551
+ Create observable slices of state for reactive workflows outside components.
552
+
553
+ ```tsx
554
+ const useStore = createGlobalState({
555
+ count: 0,
556
+ user: {
557
+ name: "Johnny",
558
+ },
559
+ });
560
+
561
+ const countObservable = useStore.createObservable((state) => state.count);
562
+
563
+ countObservable.subscribe((count) => {
564
+ console.log("Count:", count);
565
+ });
566
+
567
+ console.log(countObservable.getState());
568
+
569
+ const doubledObservable = countObservable.createObservable((count) => count * 2);
570
+ ```
571
+
572
+ #### 📋 Metadata — Non-Reactive Side Information
573
+
574
+ Metadata is useful for information that belongs to the store but should not independently trigger component updates.
575
+
576
+ ```tsx
577
+ const useFeed = createGlobalState(
578
+ { items: [] as string[] },
579
+ {
580
+ metadata: {
581
+ isLoading: false,
582
+ lastFetch: null as Date | null,
583
+ error: null as Error | null,
204
584
  },
205
585
  },
206
586
  );
207
587
  ```
208
588
 
209
- Now, instead of `setState`, the hook returns **actions**:
589
+ Read or update it through the store API:
210
590
 
211
591
  ```tsx
212
- const [filter, { setFilter }] = useContacts();
592
+ useFeed.setMetadata((metadata) => ({
593
+ ...metadata,
594
+ isLoading: true,
595
+ }));
596
+
597
+ const metadata = useFeed.getMetadata();
598
+
599
+ console.log(metadata.isLoading);
213
600
  ```
214
601
 
602
+ Persisted Native stores extend metadata with:
603
+
604
+ ```tsx
605
+ const [, , { isAsyncStorageReady, asyncStorageKey }] = useSettings();
606
+ ```
607
+
608
+ `isAsyncStorageReady` is specifically about the asynchronous persistence initialization lifecycle. Arbitrary metadata remains non-reactive.
609
+
215
610
  ---
216
611
 
217
- ## 🌍 Accessing Global State Outside Components
612
+ ### 💾 Persisted State with Async Storage
613
+
614
+ This is the major platform-specific feature of `react-native-global-state-hooks`.
615
+
616
+ Unlike browser `localStorage`, React Native persistence is **asynchronous**. A persisted store starts with its initial state, checks storage asynchronously, validates or migrates the restored value, and then marks the persistence initialization as ready.
617
+
618
+ #### 🎨 Basic Persistence
619
+
620
+ ```tsx
621
+ import { createGlobalState } from "react-native-global-state-hooks";
622
+
623
+ type Settings = {
624
+ theme: "light" | "dark";
625
+ language: string;
626
+ };
627
+
628
+ const initialSettings: Settings = {
629
+ theme: "dark",
630
+ language: "en",
631
+ };
632
+
633
+ const useSettings = createGlobalState(initialSettings, {
634
+ asyncStorage: {
635
+ key: "app-settings",
636
+
637
+ validator: ({ restored, initial }) => {
638
+ if (!restored || typeof restored !== "object") {
639
+ return initial;
640
+ }
218
641
 
219
- Use `stateControls()` to **retrieve or update state outside React components**:
642
+ const value = restored as Partial<Settings>;
643
+
644
+ if ((value.theme !== "light" && value.theme !== "dark") || typeof value.language !== "string") {
645
+ return initial;
646
+ }
647
+
648
+ return value as Settings;
649
+ },
650
+ },
651
+ });
652
+ ```
653
+
654
+ The built-in persistence flow:
655
+
656
+ - ✅ Restores the value asynchronously
657
+ - ✅ Persists state changes asynchronously
658
+ - ✅ Tracks restore readiness in store metadata
659
+ - ✅ Validates restored state
660
+ - ✅ Supports schema versioning and migration
661
+ - ✅ Supports custom error handling
662
+ - ✅ Uses a configurable async storage backend
663
+
664
+ #### ⏳ Async Restore & `isAsyncStorageReady`
665
+
666
+ Because storage access is asynchronous, the initial state can be available before persisted state has finished loading.
220
667
 
221
668
  ```tsx
222
- const [contactsRetriever, contactsApi] = useContacts.stateControls();
223
- console.log(contactsRetriever()); // Retrieves the current state
669
+ function AppSettings() {
670
+ const [settings, , { isAsyncStorageReady }] = useSettings();
671
+
672
+ if (!isAsyncStorageReady) {
673
+ return <ActivityIndicator />;
674
+ }
675
+
676
+ return <Text>Theme: {settings.theme}</Text>;
677
+ }
224
678
  ```
225
679
 
226
- #### Subscribe to changes
680
+ `isAsyncStorageReady` starts as `false` for a persisted store and becomes `true` after the storage initialization flow has completed.
681
+
682
+ That initialization can:
683
+
684
+ 1. Read the stored value
685
+ 2. Migrate it when necessary
686
+ 3. Validate the result
687
+ 4. Update the store
688
+ 5. Persist the normalized value
689
+ 6. Mark Async Storage as ready
690
+
691
+ This is fundamentally different from synchronous browser `localStorage`.
692
+
693
+ #### 🔒 Validation
694
+
695
+ The current Native persistence configuration includes a `validator`.
227
696
 
228
697
  ```tsx
229
- const unsubscribe = contactsRetriever((state) => {
230
- console.log("State updated:", state);
698
+ const useProfile = createGlobalState(initialProfile, {
699
+ asyncStorage: {
700
+ key: "profile",
701
+
702
+ validator: ({ restored, initial }) => {
703
+ if (!isProfile(restored)) {
704
+ return initial;
705
+ }
706
+
707
+ return restored;
708
+ },
709
+ },
231
710
  });
232
711
  ```
233
712
 
234
- #### Subscriptions are great when one state depends on another.
713
+ The validator receives:
714
+
715
+ - **`restored`** — the value loaded from persistence
716
+ - **`initial`** — the store's current initial value
717
+
718
+ Return behavior:
719
+
720
+ - Return a value → that value becomes the restored state
721
+ - Return `initial` → reject the persisted value and fall back
722
+ - Return `undefined` → accept the restored value as-is
723
+
724
+ The validator also runs after migration.
725
+
726
+ #### 🔄 Versioning & Migration
727
+
728
+ Persisted schemas evolve. Native persistence can migrate old values before committing them to the current store.
235
729
 
236
730
  ```tsx
237
- const useSelectedContact = createGlobalState(null, {
238
- callbacks: {
239
- onInit: ({ setState, getState }) => {
240
- contactsRetriever(
241
- (state) => state.contacts,
242
- (contacts) => {
243
- if (!contacts.has(getState())) setState(null);
244
- },
245
- );
731
+ type Preferences = {
732
+ theme: "light" | "dark";
733
+ notifications: boolean;
734
+ };
735
+
736
+ const initialPreferences: Preferences = {
737
+ theme: "dark",
738
+ notifications: true,
739
+ };
740
+
741
+ const usePreferences = createGlobalState(initialPreferences, {
742
+ asyncStorage: {
743
+ key: "preferences",
744
+
745
+ versioning: {
746
+ version: 2,
747
+
748
+ migrator: ({ legacy, initial }) => {
749
+ if (legacy && typeof legacy === "object" && "theme" in legacy) {
750
+ return {
751
+ ...initial,
752
+ theme: legacy.theme === "light" ? "light" : "dark",
753
+ };
754
+ }
755
+
756
+ return initial;
757
+ },
758
+ },
759
+
760
+ validator: ({ restored, initial }) => {
761
+ if (!restored || typeof restored !== "object") {
762
+ return initial;
763
+ }
764
+
765
+ return restored as Preferences;
246
766
  },
247
767
  },
248
768
  });
249
769
  ```
250
770
 
251
- ---
771
+ With the built-in persistence path:
252
772
 
253
- ## 🎭 Using Context for Scoped State
773
+ 1. The stored item carries its schema version
774
+ 2. A version mismatch can invoke `migrator`
775
+ 3. The migrated result goes through `validator`
776
+ 4. The normalized state is committed and persisted
254
777
 
255
- - **Scoped State** – Context state is **isolated inside the provider**.
256
- - **Same API** – Context supports **selectors, actions, and state controls**.
778
+ #### 🚨 Persistence Errors
257
779
 
258
- ### 📌 Creating a Context
780
+ Handle storage, serialization, validation, or migration failures without mixing them into your UI state.
259
781
 
260
782
  ```tsx
261
- import { createContext } from "react-global-state-hooks/createContext";
262
- export const [useCounterContext, CounterProvider] = createContext(0);
783
+ const useSettings = createGlobalState(initialSettings, {
784
+ asyncStorage: {
785
+ key: "settings",
786
+
787
+ validator: ({ restored, initial }) => {
788
+ return isSettings(restored) ? restored : initial;
789
+ },
790
+
791
+ onError(error) {
792
+ reportError(error);
793
+ },
794
+ },
795
+ });
796
+ ```
797
+
798
+ If `onError` is omitted, the library reports persistence failures to `console.error`.
799
+
800
+ #### 🔧 Custom Async Storage Manager
801
+
802
+ By default, the library attempts to use:
803
+
804
+ ```text
805
+ @react-native-async-storage/async-storage
263
806
  ```
264
807
 
265
- Wrap your app:
808
+ That package is optional. You can replace the low-level storage manager globally.
809
+
810
+ A manager works with **strings**, just like AsyncStorage:
266
811
 
267
812
  ```tsx
268
- <CounterProvider>
269
- <MyComponent />
270
- </CounterProvider>
813
+ import { asyncStorageWrapper, type AsyncStorageManager } from "react-native-global-state-hooks";
814
+
815
+ const customStorage: AsyncStorageManager = {
816
+ async getItem(key) {
817
+ return myStorage.getString(key);
818
+ },
819
+
820
+ async setItem(key, value) {
821
+ await myStorage.setString(key, value);
822
+ },
823
+ };
824
+
825
+ await asyncStorageWrapper.addAsyncStorageManager(async () => {
826
+ return customStorage;
827
+ });
271
828
  ```
272
829
 
273
- Use the context state:
830
+ Configure the manager before persisted stores need to initialize.
831
+
832
+ The library still owns formatting, restoration, validation, and version envelopes when you use this low-level manager.
833
+
834
+ #### 🧩 Per-Store Persistence Adapter
835
+
836
+ Need one store to use a completely different persistence mechanism? Use `adapter`.
837
+
838
+ Unlike the global storage manager, an adapter works with the actual **State value**, not serialized strings.
274
839
 
275
840
  ```tsx
276
- const [count] = useCounterContext();
841
+ const useSettings = createGlobalState(initialSettings, {
842
+ asyncStorage: {
843
+ key: "settings",
844
+
845
+ validator: ({ restored, initial }) => {
846
+ return isSettings(restored) ? restored : initial;
847
+ },
848
+
849
+ adapter: {
850
+ async getItem(key) {
851
+ return settingsDatabase.get(key);
852
+ },
853
+
854
+ async setItem(key, value) {
855
+ await settingsDatabase.set(key, value);
856
+ },
857
+ },
858
+ },
859
+ });
277
860
  ```
278
861
 
279
- ### 📌 Context Selectors
862
+ When an adapter is provided:
863
+
864
+ - ✅ The adapter controls storage for that store
865
+ - ✅ `getItem()` and `setItem()` work with the store's state value
866
+ - ✅ Validation still applies
867
+ - ❌ Built-in versioning/migration is bypassed
868
+ - ❌ Built-in string serialization is bypassed
280
869
 
281
- Works **just like global state**, but within the provider.
870
+ Use the global manager when you want to replace the AsyncStorage-compatible backend for the app.
871
+
872
+ Use an adapter when a specific store needs custom persistence behavior.
873
+
874
+ > **Native difference:** the Native `asyncStorage` configuration does not expose the web package's `selector` option for selective persistence. If only part of a state should be persisted, split that persisted data into its own store or implement the desired behavior through an adapter.
282
875
 
283
876
  ---
284
877
 
285
- ## 🔥 Observables: Watching State Changes
878
+ ### 2️⃣ Scoped State with `createContext`
286
879
 
287
- Observables **let you react to state changes** via subscriptions.
880
+ Sometimes state should belong to one subtree rather than the entire application.
288
881
 
289
- ### 📌 Creating an Observable
882
+ #### 🎪 The Basics
290
883
 
291
884
  ```tsx
292
- export const useCounter = createGlobalState(0);
293
- export const counterLogs = useCounter.createObservable((count) => `Counter is at ${count}`);
885
+ import { TextInput, View } from "react-native";
886
+ import { createContext } from "react-native-global-state-hooks";
887
+
888
+ const UserForm = createContext({
889
+ name: "",
890
+ email: "",
891
+ });
892
+
893
+ function App() {
894
+ return (
895
+ <UserForm.Provider>
896
+ <FormFields />
897
+ </UserForm.Provider>
898
+ );
899
+ }
900
+
901
+ function FormFields() {
902
+ const [form, setForm] = UserForm.use();
903
+
904
+ return (
905
+ <View>
906
+ <TextInput
907
+ value={form.name}
908
+ onChangeText={(name) => {
909
+ setForm((state) => ({
910
+ ...state,
911
+ name,
912
+ }));
913
+ }}
914
+ />
915
+
916
+ <TextInput
917
+ value={form.email}
918
+ onChangeText={(email) => {
919
+ setForm((state) => ({
920
+ ...state,
921
+ email,
922
+ }));
923
+ }}
924
+ />
925
+ </View>
926
+ );
927
+ }
294
928
  ```
295
929
 
296
- ### 📌 Subscribing to an Observable
930
+ #### 🎁 Provider Variations
931
+
932
+ Use the default value:
297
933
 
298
934
  ```tsx
299
- const unsubscribe = counterLogs((message) => {
300
- console.log(message);
935
+ <Theme.Provider>
936
+ <Screen />
937
+ </Theme.Provider>
938
+ ```
939
+
940
+ Provide a value for one subtree:
941
+
942
+ ```tsx
943
+ <Theme.Provider value="dark">
944
+ <Screen />
945
+ </Theme.Provider>
946
+ ```
947
+
948
+ Or derive a value from the context's initial value:
949
+
950
+ ```tsx
951
+ <Theme.Provider value={(initial) => (initial === "dark" ? "light" : "dark")}>
952
+ <Screen />
953
+ </Theme.Provider>
954
+ ```
955
+
956
+ #### 🎯 Context + Selectors = ❤️
957
+
958
+ ```tsx
959
+ const Profile = createContext({
960
+ user: {
961
+ name: "",
962
+ email: "",
963
+ },
964
+ settings: {
965
+ darkMode: false,
966
+ },
301
967
  });
968
+
969
+ function UserName() {
970
+ const [name] = Profile.use((state) => state.user.name);
971
+
972
+ return <Text>{name}</Text>;
973
+ }
302
974
  ```
303
975
 
304
- ### 📌 Using Observables Inside Context
976
+ #### 🎭 Context with Actions
305
977
 
306
978
  ```tsx
307
- export const [useStateControls, useObservableBuilder] = useCounterContext.stateControls();
308
- const createObservable = useObservableBuilder();
309
- useEffect(() => {
310
- const unsubscribe = createObservable((count) => {
311
- console.log(`Updated count: ${count}`);
312
- });
313
- return unsubscribe;
314
- }, []);
979
+ const Counter = createContext(0, {
980
+ actions: {
981
+ increment(amount = 1) {
982
+ return ({ setState, getState }) => {
983
+ setState(getState() + amount);
984
+ };
985
+ },
986
+
987
+ reset() {
988
+ return ({ setState }) => {
989
+ setState(0);
990
+ };
991
+ },
992
+ },
993
+ });
994
+
995
+ function CounterScreen() {
996
+ const [count, actions] = Counter.use();
997
+
998
+ return (
999
+ <View>
1000
+ <Text>{count}</Text>
1001
+
1002
+ <Button title="+1" onPress={() => actions.increment()} />
1003
+
1004
+ <Button title="Reset" onPress={actions.reset} />
1005
+ </View>
1006
+ );
1007
+ }
315
1008
  ```
316
1009
 
317
- ---
1010
+ #### 🔗 Reusable Context Selectors
1011
+
1012
+ ```tsx
1013
+ const Data = createContext({
1014
+ users: [] as Array<{
1015
+ id: string;
1016
+ name: string;
1017
+ active: boolean;
1018
+ }>,
1019
+ });
318
1020
 
319
- ## ⚖️ `createGlobalState` vs. `createContext`
1021
+ const useUsers = Data.use.createSelectorHook((state) => state.users);
320
1022
 
321
- | Feature | `createGlobalState` | `createContext` |
322
- | ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
323
- | **Scope** | Available globally across the entire app | Scoped to the Provider where it’s used |
324
- | **How to Use** | `const useCount = createGlobalState(0)` | `const [useCountContext, Provider] = createContext(0)` |
325
- | **createSelectorHook** | `useCount.createSelectorHook` | `useCountContext.createSelectorHook` |
326
- | **inline selectors?** | ✅ Supported | ✅ Supported |
327
- | **Custom Actions** | ✅ Supported | ✅ Supported |
328
- | **Observables** | `useCount.createObservable` | `const [, useObservableBuilder] = useCountContext.stateControls()` |
329
- | **State Controls** | `useCount.stateControls()` | `const [useStateControls] = useCountContext.stateControls()` |
330
- | **Best For** | Global app state (auth, settings, cache) | Scoped module state, reusable component state, or state shared between child components without being fully global |
1023
+ const useActiveUsers = useUsers.createSelectorHook((users) => users.filter((user) => user.active));
331
1024
 
332
- ## 🔄 Lifecycle Methods
1025
+ function ActiveUsers() {
1026
+ const [users] = useActiveUsers();
333
1027
 
334
- Global state hooks support lifecycle callbacks for additional control.
1028
+ return (
1029
+ <View>
1030
+ {users.map((user) => (
1031
+ <Text key={user.id}>{user.name}</Text>
1032
+ ))}
1033
+ </View>
1034
+ );
1035
+ }
1036
+ ```
1037
+
1038
+ #### 🧪 Testing Context Stores
1039
+
1040
+ The Provider can expose its store tools through a wrapper helper.
1041
+
1042
+ ```tsx
1043
+ const { wrapper, context } = Counter.Provider.makeProviderWrapper();
1044
+
1045
+ // Use `wrapper` with your hook/component test renderer.
1046
+
1047
+ // Direct access to the context API:
1048
+ context.current.actions.increment();
1049
+
1050
+ console.log(context.current.getState());
1051
+ ```
1052
+
1053
+ #### 🎬 Lifecycle Hooks
1054
+
1055
+ Context stores can react to Provider creation, mount, and cleanup.
335
1056
 
336
1057
  ```tsx
337
- const useData = createGlobalState(
338
- { value: 1 },
1058
+ const Session = createContext(
1059
+ {
1060
+ user: null,
1061
+ },
339
1062
  {
340
1063
  callbacks: {
341
- onInit: ({ setState }) => {
342
- console.log("Store initialized");
1064
+ onCreated(api) {
1065
+ console.log("Context created:", api.getState());
343
1066
  },
344
- onStateChanged: ({ state, previousState }) => {
345
- console.log("State changed:", previousState, "→", state);
346
- },
347
- computePreventStateChange: ({ state, previousState }) => {
348
- return state.value === previousState.value;
1067
+
1068
+ onMounted(api) {
1069
+ const unsubscribe = sessionEvents.subscribe((user) => {
1070
+ api.setState({ user });
1071
+ });
1072
+
1073
+ return unsubscribe;
349
1074
  },
350
1075
  },
351
1076
  },
352
1077
  );
353
1078
  ```
354
1079
 
355
- Use **`onInit`** for setup, **`onStateChanged`** to listen to updates, and **`computePreventStateChange`** to prevent unnecessary updates.
1080
+ ---
356
1081
 
357
- ## Metadata
1082
+ ## 🔥 Advanced Patterns
1083
+
1084
+ ### 🏗️ Production Architecture
1085
+
1086
+ A larger application can keep each domain store self-contained.
1087
+
1088
+ ```text
1089
+ src/stores/todos/
1090
+ ├── index.ts
1091
+ ├── todos$.ts
1092
+ ├── constants/
1093
+ │ └── initialValue.ts
1094
+ ├── types/
1095
+ │ └── Todo.ts
1096
+ ├── hooks/
1097
+ │ ├── useActiveTodos.ts
1098
+ │ └── useCompletedTodos.ts
1099
+ └── helpers/
1100
+ └── createTodo.ts
1101
+ ```
358
1102
 
359
- There is a possibility to add non reactive information in the global state:
1103
+ **`todos$.ts`**
360
1104
 
361
1105
  ```tsx
362
- const useCount = createGlobalState(0, { metadata: { renders: 0 } });
1106
+ import { createGlobalState } from "react-native-global-state-hooks";
1107
+ import { initialValue } from "./constants/initialValue";
1108
+
1109
+ const todos$ = createGlobalState(initialValue, {
1110
+ asyncStorage: {
1111
+ key: "todos",
1112
+
1113
+ validator: ({ restored, initial }) => {
1114
+ return isTodosState(restored) ? restored : initial;
1115
+ },
1116
+ },
1117
+
1118
+ actions: {
1119
+ addTodo(text: string) {
1120
+ return ({ setState }) => {
1121
+ setState((state) => ({
1122
+ ...state,
1123
+ todos: [
1124
+ ...state.todos,
1125
+ {
1126
+ id: `${Date.now()}`,
1127
+ text,
1128
+ completed: false,
1129
+ },
1130
+ ],
1131
+ }));
1132
+ };
1133
+ },
1134
+
1135
+ toggleTodo(id: string) {
1136
+ return ({ setState }) => {
1137
+ setState((state) => ({
1138
+ ...state,
1139
+ todos: state.todos.map((todo) =>
1140
+ todo.id === id
1141
+ ? {
1142
+ ...todo,
1143
+ completed: !todo.completed,
1144
+ }
1145
+ : todo,
1146
+ ),
1147
+ }));
1148
+ };
1149
+ },
1150
+ },
1151
+ });
1152
+
1153
+ export default todos$;
363
1154
  ```
364
1155
 
365
- How to use it?
1156
+ **`hooks/useActiveTodos.ts`**
366
1157
 
367
1158
  ```tsx
368
- const [count, , metadata] = useCount();
1159
+ import todos$ from "../todos$";
1160
+
1161
+ export const useActiveTodos = todos$.createSelectorHook((state) =>
1162
+ state.todos.filter((todo) => !todo.completed),
1163
+ );
1164
+ ```
1165
+
1166
+ **`index.ts`**
369
1167
 
370
- metadata.renders += 1;
1168
+ ```tsx
1169
+ import todos$ from "./todos$";
1170
+ import { useActiveTodos } from "./hooks/useActiveTodos";
1171
+ import { useCompletedTodos } from "./hooks/useCompletedTodos";
1172
+
1173
+ export default Object.assign(todos$, {
1174
+ useActiveTodos,
1175
+ useCompletedTodos,
1176
+ }); // groups everything into a single name space
371
1177
  ```
372
1178
 
373
- ## 🎯 Ready to Try It?
1179
+ Usage:
374
1180
 
375
- 📦 **NPM Package:** [react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)
1181
+ ```tsx
1182
+ import todos$ from "./todos";
1183
+
1184
+ function TodoList() {
1185
+ const activeTodos = todos$.useActiveTodos();
1186
+
1187
+ return (
1188
+ <View>
1189
+ {activeTodos.map((todo) => (
1190
+ <Text key={todo.id}>{todo.text}</Text>
1191
+ ))}
1192
+ </View>
1193
+ );
1194
+ }
1195
+ ```
376
1196
 
377
- 🚀 Simplify your **global state management** in React & React Native today! 🚀
1197
+ ### 🎧 Smart Subscriptions
378
1198
 
379
- # Using async persist storage
1199
+ Subscribe to only the fragment an integration needs.
380
1200
 
381
- ```ts
382
- const useCount = createGlobalState(0, {
383
- asyncStorage: {
384
- key: "count",
1201
+ ```tsx
1202
+ const useSession = createGlobalState({
1203
+ user: null as null | { id: string; role: string },
1204
+ isOnline: true,
1205
+ });
1206
+
1207
+ const unsubscribeRole = useSession.subscribe(
1208
+ (state) => state.user?.role,
1209
+ (role) => {
1210
+ analytics.track("role_changed", {
1211
+ role,
1212
+ });
385
1213
  },
1214
+ );
1215
+
1216
+ const unsubscribeConnection = useSession.subscribe(
1217
+ (state) => state.isOnline,
1218
+ (isOnline) => {
1219
+ syncEngine.setOnline(isOnline);
1220
+ },
1221
+ );
1222
+
1223
+ // Cleanup
1224
+ unsubscribeRole();
1225
+ unsubscribeConnection();
1226
+ ```
1227
+
1228
+ ### 📲 App Lifecycle Integration
1229
+
1230
+ The non-reactive API is useful when React Native lifecycle events happen outside a screen.
1231
+
1232
+ ```tsx
1233
+ import { AppState } from "react-native";
1234
+
1235
+ const useAppStatus = createGlobalState({
1236
+ active: true,
1237
+ });
1238
+
1239
+ const subscription = AppState.addEventListener("change", (status) => {
1240
+ useAppStatus.setState({
1241
+ active: status === "active",
1242
+ });
1243
+ });
1244
+
1245
+ // Later
1246
+ subscription.remove();
1247
+ ```
1248
+
1249
+ ---
1250
+
1251
+ ## 🎨 `uniqueId` — Type-Safe Unique IDs
1252
+
1253
+ `react-native-global-state-hooks` also exports `uniqueId`.
1254
+
1255
+ ### 🏷️ Basic Usage
1256
+
1257
+ ```tsx
1258
+ import { uniqueId } from "react-native-global-state-hooks";
1259
+
1260
+ const id = uniqueId();
1261
+
1262
+ console.log(id);
1263
+ ```
1264
+
1265
+ Add a prefix:
1266
+
1267
+ ```tsx
1268
+ const userId = uniqueId("user:");
1269
+ const todoId = uniqueId("todo:");
1270
+ ```
1271
+
1272
+ ### 🔒 Branded IDs
1273
+
1274
+ Create identifiers that TypeScript treats as different domains.
1275
+
1276
+ ```tsx
1277
+ import { uniqueId } from "react-native-global-state-hooks";
1278
+
1279
+ const userId = uniqueId.for("user:");
1280
+ const todoId = uniqueId.for("todo:");
1281
+
1282
+ type UserId = ReturnType<typeof userId>;
1283
+ type TodoId = ReturnType<typeof todoId>;
1284
+ ```
1285
+
1286
+ Now accidentally mixing identifiers becomes a type error:
1287
+
1288
+ ```tsx
1289
+ function openUser(id: UserId) {
1290
+ // ...
1291
+ }
1292
+
1293
+ openUser(userId); // ✅
1294
+ openUser(todoId); // ❌ TypeScript error
1295
+ ```
1296
+
1297
+ ### 💼 Real-World Example
1298
+
1299
+ ```tsx
1300
+ type User = {
1301
+ id: UserId;
1302
+ name: string;
1303
+ };
1304
+
1305
+ type Todo = {
1306
+ id: TodoId;
1307
+ text: string;
1308
+ assignedTo: UserId | null;
1309
+ };
1310
+
1311
+ const useApp = createGlobalState({
1312
+ users: [] as User[],
1313
+ todos: [] as Todo[],
386
1314
  });
387
1315
  ```
388
1316
 
389
- Inside your components
1317
+ ---
1318
+
1319
+ ## 🎓 Learning Resources
1320
+
1321
+ | Resource | Description |
1322
+ | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
1323
+ | 📦 [**NPM Package**](https://www.npmjs.com/package/react-native-global-state-hooks) | Published React Native package |
1324
+ | 💻 [**GitHub Repository**](https://github.com/johnny-quesada-developer/react-native-global-state-hooks) | Source, tests, and issues |
1325
+ | 🎮 [**Core API Demo**](https://johnny-quesada-developer.github.io/global-hooks-example/) | Browser demo of the shared state-management concepts |
1326
+ | 🎥 [**Video Tutorial**](https://www.youtube.com/watch?v=1UBqXk2MH8I/) | State-management walkthrough |
1327
+ | 📚 [**Core Package**](https://www.npmjs.com/package/react-hooks-global-states) | Shared state engine used by the platform packages |
1328
+
1329
+ ---
1330
+
1331
+ ## 🌐 Platform-Specific Versions
1332
+
1333
+ | Package | Platform | Persistence |
1334
+ | -------------------------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------- |
1335
+ | [`react-hooks-global-states`](https://www.npmjs.com/package/react-hooks-global-states) | Core React state library | No platform-specific persistence layer |
1336
+ | [`react-global-state-hooks`](https://www.npmjs.com/package/react-global-state-hooks) | Web | `localStorage` integration |
1337
+ | [`react-native-global-state-hooks`](https://www.npmjs.com/package/react-native-global-state-hooks) | React Native | Async persistence with optional AsyncStorage backend |
1338
+
1339
+ The state-management concepts are intentionally similar across the packages, but their persistence models are **not interchangeable**.
1340
+
1341
+ ---
1342
+
1343
+ ## 🎉 Why Developers Choose This
1344
+
1345
+ ### The Bottom Line
1346
+
1347
+ | What You Get | What You Avoid |
1348
+ | ----------------------------------- | ---------------------------------- |
1349
+ | ✅ `useState`-like API | ❌ Redux-style boilerplate |
1350
+ | ✅ Surgical selectors | ❌ Whole-store re-renders |
1351
+ | ✅ Chainable selector hooks | ❌ Repeated selector logic |
1352
+ | ✅ Optional actions | ❌ Forced architecture |
1353
+ | ✅ Global + scoped context state | ❌ Choosing only one model |
1354
+ | ✅ Non-reactive store API | ❌ React-only access |
1355
+ | ✅ Async native persistence | ❌ Hand-written restore plumbing |
1356
+ | ✅ `isAsyncStorageReady` | ❌ Guessing when restore completed |
1357
+ | ✅ Validation + migration | ❌ Fragile persisted schemas |
1358
+ | ✅ Custom storage manager / adapter | ❌ One forced persistence backend |
1359
+ | ✅ TypeScript inference | ❌ Manual state contracts |
1360
+
1361
+ ---
1362
+
1363
+ ## 🚀 Get Started Now
1364
+
1365
+ ```bash
1366
+ npm install react-native-global-state-hooks
1367
+ ```
1368
+
1369
+ or
1370
+
1371
+ ```bash
1372
+ yarn add react-native-global-state-hooks
1373
+ ```
1374
+
1375
+ Then:
390
1376
 
391
1377
  ```tsx
392
- const [count, setCount, { isAsyncStorageReady }] = useCount();
1378
+ import { Button } from "react-native";
1379
+ import { createGlobalState } from "react-native-global-state-hooks";
1380
+
1381
+ const useCounter = createGlobalState(0);
1382
+
1383
+ function App() {
1384
+ const [count, setCount] = useCounter();
1385
+
1386
+ return (
1387
+ <Button
1388
+ title={`Count: ${count}`}
1389
+ onPress={() => {
1390
+ setCount((current) => current + 1);
1391
+ }}
1392
+ />
1393
+ );
1394
+ }
393
1395
  ```
394
1396
 
395
- - if you specify a key into the `asyncStorage` this will persist the state value if the same is serializable
396
- - when connecting to the async storage you can expect a second render which will update isAsyncStorageReady indicating that the async storage was already reviewed and the state value is committed.
1397
+ **That's it. You're managing shared React Native state.** 🎉
1398
+
1399
+ ---
1400
+
1401
+ <div align="center">
1402
+
1403
+ ### Built with ❤️ for developers who value simplicity
397
1404
 
398
- The async storage default functionality depends on **@react-native-async-storage/async-storage** but this dependency is optional, install the package as a dependency if you want to enable persisted state.
1405
+ **[⭐ Star on GitHub](https://github.com/johnny-quesada-developer/react-native-global-state-hooks)** **[📝 Report Issues](https://github.com/johnny-quesada-developer/react-native-global-state-hooks/issues)** **[📦 NPM](https://www.npmjs.com/package/react-native-global-state-hooks)**
399
1406
 
400
- optionally you can configure your own selection for persisting storage by using `asyncStorageWrapper.addAsyncStorageManager`, notice that the manager should be added before any hook gets call;
1407
+ </div>