react-native-global-state-hooks 15.0.2 → 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/GlobalStore.js +1 -1
- package/README.md +1209 -202
- package/asyncStorageWrapper.js +1 -1
- package/bundle.js +1 -1
- package/createContext.js +1 -1
- package/createGlobalState.js +1 -1
- package/isRecord.js +1 -1
- package/package.json +3 -2
- package/shallowCompare.js +1 -1
- package/throwWrongKeyOnActionCollectionConfig.js +1 -1
- package/types.js +1 -1
- package/uniqueId.js +1 -1
package/README.md
CHANGED
|
@@ -1,400 +1,1407 @@
|
|
|
1
1
|
# react-native-global-state-hooks 🌟
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
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
|
-
|
|
15
|
+
[](https://www.npmjs.com/package/react-native-global-state-hooks)
|
|
16
|
+
[](https://www.npmjs.com/package/react-native-global-state-hooks)
|
|
17
|
+
[](https://github.com/johnny-quesada-developer/react-native-global-state-hooks/blob/master/LICENSE)
|
|
10
18
|
|
|
11
|
-
- **[
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** specific for web applications (**local-storage integration**).
|
|
15
|
-
- **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** specific for React Native projects (**async-storage integration**).
|
|
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
|
-
##
|
|
25
|
+
## 🎯 The One-Liner
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
```tsx
|
|
28
|
+
import { createGlobalState } from "react-native-global-state-hooks";
|
|
22
29
|
|
|
23
|
-
|
|
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
|
-
|
|
38
|
+
function Counter() {
|
|
39
|
+
const [count, setCount] = useCounter();
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
-
|  |  |
|
|
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
|
-
|
|
49
|
+
### 🎓 **Zero Learning Curve**
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
If you know `useState`, the basic API already feels familiar:
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
```tsx
|
|
54
|
+
// Local state
|
|
55
|
+
const [count, setCount] = useState(0);
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
asyncStorage: {
|
|
48
|
-
key: "count",
|
|
49
|
-
},
|
|
50
|
-
});
|
|
57
|
+
// Global state
|
|
58
|
+
const [count, setCount] = useCounter();
|
|
51
59
|
```
|
|
52
60
|
|
|
53
|
-
###
|
|
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
|
-
|
|
63
|
+
Subscribe to only the part of the state your component needs.
|
|
61
64
|
|
|
62
65
|
```tsx
|
|
63
|
-
const [
|
|
66
|
+
const [name] = useStore((state) => state.user.name);
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
The component only needs to react when its selected value changes.
|
|
67
70
|
|
|
68
|
-
###
|
|
71
|
+
### 🔗 **Chainable Selectors**
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
Build reusable state hooks from other selector hooks.
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
```tsx
|
|
76
|
+
const useUsers = useStore.createSelectorHook((state) => state.users);
|
|
73
77
|
|
|
74
|
-
|
|
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
|
-
|
|
81
|
+
### 🎭 **Actions (Optional)**
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
Keep mutation logic close to the store when you want more structure.
|
|
82
84
|
|
|
83
85
|
```tsx
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
92
|
-
|
|
102
|
+
const Form = createContext({
|
|
103
|
+
name: "",
|
|
104
|
+
email: "",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
<Form.Provider>
|
|
108
|
+
<FormFields />
|
|
109
|
+
</Form.Provider>;
|
|
93
110
|
```
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
### 📱 **Native Async Persistence**
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
Persist state with React Native async storage semantics.
|
|
98
115
|
|
|
99
|
-
|
|
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
|
-
|
|
133
|
+
Read, update, or subscribe to state outside React components.
|
|
102
134
|
|
|
103
135
|
```tsx
|
|
104
|
-
|
|
136
|
+
const currentUser = useAuth.getState().user;
|
|
137
|
+
|
|
138
|
+
useAuth.setState((state) => ({
|
|
139
|
+
...state,
|
|
140
|
+
user: nextUser,
|
|
141
|
+
}));
|
|
105
142
|
```
|
|
106
143
|
|
|
107
|
-
|
|
144
|
+
---
|
|
108
145
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
## 🎬 Quick Start
|
|
179
|
+
|
|
180
|
+
### 30 Seconds to Global State
|
|
123
181
|
|
|
124
182
|
```tsx
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
291
|
+
#### 🎨 The Basics
|
|
147
292
|
|
|
148
293
|
```tsx
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
314
|
+
#### 🎯 Surgical Re-renders with Selectors
|
|
154
315
|
|
|
155
316
|
```tsx
|
|
156
|
-
const
|
|
157
|
-
|
|
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
|
-
|
|
349
|
+
Each component subscribes to the value it actually uses instead of blindly reacting to the whole state object.
|
|
161
350
|
|
|
162
|
-
|
|
351
|
+
#### ⚡ Computed Values with Dependencies
|
|
352
|
+
|
|
353
|
+
Selectors can also depend on component-local values.
|
|
163
354
|
|
|
164
355
|
```tsx
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
####
|
|
405
|
+
#### 🔗 Reusable Selector Hooks
|
|
172
406
|
|
|
173
|
-
|
|
407
|
+
Create hooks from hooks and compose them.
|
|
174
408
|
|
|
175
409
|
```tsx
|
|
176
|
-
const
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
return
|
|
195
|
-
const
|
|
196
|
-
|
|
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
|
-
|
|
473
|
+
|
|
474
|
+
toggleTodo(id: string) {
|
|
200
475
|
return ({ setState }) => {
|
|
201
|
-
setState((state) => ({
|
|
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
|
-
|
|
589
|
+
Read or update it through the store API:
|
|
210
590
|
|
|
211
591
|
```tsx
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
- **Same API** – Context supports **selectors, actions, and state controls**.
|
|
778
|
+
#### 🚨 Persistence Errors
|
|
257
779
|
|
|
258
|
-
|
|
780
|
+
Handle storage, serialization, validation, or migration failures without mixing them into your UI state.
|
|
259
781
|
|
|
260
782
|
```tsx
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
878
|
+
### 2️⃣ Scoped State with `createContext`
|
|
286
879
|
|
|
287
|
-
|
|
880
|
+
Sometimes state should belong to one subtree rather than the entire application.
|
|
288
881
|
|
|
289
|
-
|
|
882
|
+
#### 🎪 The Basics
|
|
290
883
|
|
|
291
884
|
```tsx
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
930
|
+
#### 🎁 Provider Variations
|
|
931
|
+
|
|
932
|
+
Use the default value:
|
|
297
933
|
|
|
298
934
|
```tsx
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
976
|
+
#### 🎭 Context with Actions
|
|
305
977
|
|
|
306
978
|
```tsx
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
1021
|
+
const useUsers = Data.use.createSelectorHook((state) => state.users);
|
|
320
1022
|
|
|
321
|
-
|
|
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
|
-
|
|
1025
|
+
function ActiveUsers() {
|
|
1026
|
+
const [users] = useActiveUsers();
|
|
333
1027
|
|
|
334
|
-
|
|
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
|
|
338
|
-
{
|
|
1058
|
+
const Session = createContext(
|
|
1059
|
+
{
|
|
1060
|
+
user: null,
|
|
1061
|
+
},
|
|
339
1062
|
{
|
|
340
1063
|
callbacks: {
|
|
341
|
-
|
|
342
|
-
console.log("
|
|
1064
|
+
onCreated(api) {
|
|
1065
|
+
console.log("Context created:", api.getState());
|
|
343
1066
|
},
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
1080
|
+
---
|
|
356
1081
|
|
|
357
|
-
##
|
|
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
|
-
|
|
1103
|
+
**`todos$.ts`**
|
|
360
1104
|
|
|
361
1105
|
```tsx
|
|
362
|
-
|
|
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
|
-
|
|
1156
|
+
**`hooks/useActiveTodos.ts`**
|
|
366
1157
|
|
|
367
1158
|
```tsx
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1179
|
+
Usage:
|
|
374
1180
|
|
|
375
|
-
|
|
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
|
-
|
|
1197
|
+
### 🎧 Smart Subscriptions
|
|
378
1198
|
|
|
379
|
-
|
|
1199
|
+
Subscribe to only the fragment an integration needs.
|
|
380
1200
|
|
|
381
|
-
```
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1407
|
+
</div>
|