react-native-global-state-hooks 8.0.2 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GlobalStore.d.ts +1 -0
- package/GlobalStore.js +1 -1
- package/GlobalStoreAbstract.d.ts +1 -0
- package/GlobalStoreAbstract.js +1 -1
- package/README.md +216 -407
- package/asyncStorageWrapper.d.ts +1 -1
- package/asyncStorageWrapper.js +1 -1
- package/bundle.js +1 -1
- package/createContext.d.ts +1 -1
- package/createContext.js +1 -1
- package/createCustomGlobalState.d.ts +1 -0
- package/createCustomGlobalState.js +1 -1
- package/createGlobalState.d.ts +1 -0
- package/createGlobalState.js +1 -1
- package/debounce.d.ts +1 -1
- package/debounce.js +1 -1
- package/getAsyncStorageItem.d.ts +1 -0
- package/getAsyncStorageItem.js +1 -1
- package/index.d.ts +1 -3
- package/isRecord.d.ts +1 -1
- package/isRecord.js +1 -1
- package/package.json +3 -13
- package/setAsyncStorageItem.d.ts +1 -0
- package/setAsyncStorageItem.js +1 -1
- package/shallowCompare.d.ts +1 -1
- package/shallowCompare.js +1 -1
- package/throwWrongKeyOnActionCollectionConfig.d.ts +1 -1
- package/throwWrongKeyOnActionCollectionConfig.js +1 -1
- package/uniqueId.d.ts +1 -1
- package/uniqueId.js +1 -1
- package/uniqueSymbol.d.ts +1 -1
- package/uniqueSymbol.js +1 -1
- package/useStableState.d.ts +1 -0
- package/useStableState.js +1 -0
- package/webpack.config.js +1 -3
- package/combineRetrieverAsynchronously.d.ts +0 -1
- package/combineRetrieverAsynchronously.js +0 -1
- package/combineRetrieverEmitterAsynchronously.d.ts +0 -1
- package/combineRetrieverEmitterAsynchronously.js +0 -1
- package/useConstantValueRef.d.ts +0 -1
- package/useConstantValueRef.js +0 -1
package/README.md
CHANGED
|
@@ -2,88 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
|
|
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! ✨
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 🔗 Explore More
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- **[Live Example](https://johnny-quesada-developer.github.io/react-global-state-hooks-example/)** 📘
|
|
12
|
+
- **[React Native Integration](https://www.npmjs.com/package/react-native-global-state-hooks/)** 📱
|
|
13
|
+
- **[Todo-List Example](https://github.com/johnny-quesada-developer/todo-list-with-global-hooks.git/)** 📝
|
|
14
|
+
- **[Video Overview](https://www.youtube.com/watch?v=1UBqXk2MH8I/)** 🎥
|
|
15
|
+
- **[GitHub Repository](https://github.com/johnny-quesada-developer/global-hooks-example/)** 🧩
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Works seamlessly with **React & React Native**:
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
- **[react-global-state-hooks](https://www.npmjs.com/package/react-global-state-hooks)** for web applications.
|
|
20
|
+
- **[react-native-global-state-hooks](https://www.npmjs.com/package/react-native-global-state-hooks)** for React Native projects.
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
---
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
## 🗂️ Async Persist Storage
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
We are gonna create a global state hook **useCount** with one line of code.
|
|
26
|
+
To persist the global state using **Async Storage**, simply add the `asyncStorage` option:
|
|
24
27
|
|
|
25
28
|
```ts
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const useCount = createGlobalState(0, {
|
|
30
|
+
asyncStorage: {
|
|
31
|
+
key: "count",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
### 🔹 How It Works
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
✅ **Automatically syncs the state with Async Storage** if the value is serializable.
|
|
39
|
+
✅ **Provides an `isAsyncStorageReady` flag** to indicate when the async storage has been reviewed and committed.
|
|
40
|
+
✅ **Uses `@react-native-async-storage/async-storage` by default** (make sure to install this package if needed).
|
|
41
|
+
✅ **Allows custom async storage managers** with `asyncStorageWrapper.addAsyncStorageManager(customAsyncStorageManager)`;
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
const [count, setCount] = useCount();
|
|
43
|
+
Inside your components:
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
```tsx
|
|
46
|
+
const [count, setCount, { isAsyncStorageReady }] = useCount();
|
|
39
47
|
```
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
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.
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
### 🔧 Custom Async Storage Manager
|
|
52
|
+
|
|
53
|
+
You can configure your own storage selection by using `asyncStorageWrapper.addAsyncStorageManager`. Ensure that the manager is **added before any hook is called**.
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
`index.ts`
|
|
46
56
|
|
|
47
57
|
```ts
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
key: "count",
|
|
51
|
-
},
|
|
52
|
-
});
|
|
58
|
+
import { asyncStorageWrapper } from "react-global-state-hooks";
|
|
59
|
+
asyncStorageWrapper.addAsyncStorageManager(customAsyncStorageManager);
|
|
53
60
|
```
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
## 🛠 Creating a Global State
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
Define a **global state** in **one line**:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { createGlobalState } from "react-hooks-global-states/createGlobalState";
|
|
68
|
+
export const useCount = createGlobalState(0);
|
|
59
69
|
```
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
Now, use it inside a component:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const [count, setCount] = useCount();
|
|
75
|
+
return <Button onClick={() => setCount((count) => count + 1)}>{count}</Button>;
|
|
76
|
+
```
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
The metadata and components will be updated and re-rendered even if there's no difference between the value stored in **AsyncStorage** and the store's default value. This is the only instance where the metadata forces a re-render, after this the metadata will not update the component
|
|
78
|
+
Works just like **useState**, but the **state is shared globally**! 🎉
|
|
65
79
|
|
|
66
|
-
|
|
80
|
+
---
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
## 🎯 Selectors: Subscribing to Specific State Changes
|
|
69
83
|
|
|
70
|
-
|
|
71
|
-
import { createGlobalState } from 'react-hooks-global-states';
|
|
84
|
+
For **complex state objects**, you can subscribe to specific properties instead of the entire state:
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
entities: Contact[],
|
|
76
|
-
selected: Set<number>,
|
|
77
|
-
});
|
|
86
|
+
```tsx
|
|
87
|
+
export const useContacts = createGlobalState({ entities: [], selected: new Set<number>() });
|
|
78
88
|
```
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
To access only the `entities` property:
|
|
81
91
|
|
|
82
92
|
```tsx
|
|
83
|
-
|
|
84
|
-
// and the component will only re-render if the property **entities** changes on the global state
|
|
85
|
-
const [contacts] = useContacts((state) => state.entities]);
|
|
86
|
-
|
|
93
|
+
const [contacts] = useContacts((state) => state.entities);
|
|
87
94
|
return (
|
|
88
95
|
<ul>
|
|
89
96
|
{contacts.map((contact) => (
|
|
@@ -93,312 +100,152 @@ return (
|
|
|
93
100
|
);
|
|
94
101
|
```
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
export const useContacts = createGlobalState({
|
|
100
|
-
isLoading: true,
|
|
101
|
-
entities: Map<number, Contact>,
|
|
102
|
-
selected: Set<number>,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// The selector is simply a standard selector used to extract the values from the map.
|
|
106
|
-
|
|
107
|
-
const [contacts] = useContacts((state) => [...state.entities.values()], {
|
|
108
|
-
// The isEqualRoots function allows you to create your own validation logic for determining when to recompute the selector.
|
|
109
|
-
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Okay, everything works when the changes come from the state, but what happens if I want to recompute the selector based on the internal state of the component?
|
|
103
|
+
### 📌 Using Dependencies in Selectors
|
|
114
104
|
|
|
115
|
-
**
|
|
105
|
+
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:
|
|
116
106
|
|
|
117
107
|
```tsx
|
|
118
|
-
const [filter, setFilter] = useState("");
|
|
119
|
-
|
|
120
108
|
const [contacts] = useContacts(
|
|
121
|
-
(state) =>
|
|
122
|
-
|
|
123
|
-
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
124
|
-
/**
|
|
125
|
-
* Easy to understand, right? With the dependencies prop, you can,
|
|
126
|
-
* just like with any other hook, provide a collection of values that will be compared during each render cycle
|
|
127
|
-
* to determine if the selector should be recomputed.*/
|
|
128
|
-
dependencies: [filter],
|
|
129
|
-
}
|
|
109
|
+
(state) => state.entities.filter((item) => item.name.includes(filter)),
|
|
110
|
+
[filter]
|
|
130
111
|
);
|
|
131
112
|
```
|
|
132
113
|
|
|
133
|
-
|
|
114
|
+
Alternatively, you can pass dependencies inside an **options object**:
|
|
134
115
|
|
|
135
116
|
```tsx
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
entities: Map<number, Contact>,
|
|
139
|
-
selected: Set<number>,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const useContactsArray = useContacts.createSelectorHook((state) => [...state.entities.values()], {
|
|
117
|
+
const [contacts] = useContacts((state) => state.entities.filter((item) => item.name.includes(filter)), {
|
|
118
|
+
dependencies: [filter],
|
|
143
119
|
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
144
120
|
});
|
|
145
121
|
```
|
|
146
122
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
**component.ts**
|
|
123
|
+
Unlike Redux, where only **root state changes trigger re-selection**, this approach ensures that **derived values recompute when dependencies change** while maintaining performance.
|
|
150
124
|
|
|
151
|
-
|
|
152
|
-
const [filter, setFilter] = useState("");
|
|
125
|
+
---
|
|
153
126
|
|
|
154
|
-
|
|
155
|
-
dependencies: [filter],
|
|
156
|
-
});
|
|
157
|
-
```
|
|
127
|
+
## 🔄 Reusing Selectors
|
|
158
128
|
|
|
159
|
-
|
|
129
|
+
### 📌 Creating a Selector
|
|
160
130
|
|
|
161
|
-
```
|
|
162
|
-
const useContactsArray = useContacts.createSelectorHook((state) =>
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const useContactsLength = useContactsArray.createSelectorHook((entities) => entities.length);
|
|
131
|
+
```tsx
|
|
132
|
+
export const useContactsArray = useContacts.createSelectorHook((state) => state.entities);
|
|
133
|
+
export const useContactsCount = useContactsArray.createSelectorHook((entities) => entities.length);
|
|
167
134
|
```
|
|
168
135
|
|
|
169
|
-
|
|
136
|
+
### 📌 Using Selectors in Components
|
|
170
137
|
|
|
171
138
|
```tsx
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
dependencies: [filter],
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
return contacts;
|
|
178
|
-
};
|
|
139
|
+
const [contacts] = useContactsArray();
|
|
140
|
+
const [count] = useContactsCount();
|
|
179
141
|
```
|
|
180
142
|
|
|
181
|
-
|
|
143
|
+
#### ✅ Selectors support inline selectors and dependencies
|
|
182
144
|
|
|
183
|
-
|
|
184
|
-
const [filter, setFilter] = useState("");
|
|
185
|
-
|
|
186
|
-
const [contacts] = useContacts((state) => state.contacts.filter((contact) => contact.name.includes(filter)), {
|
|
187
|
-
/**
|
|
188
|
-
* You can use the `isEqualRoot` to validate if the values before the selector are equal.
|
|
189
|
-
* This validation will run before `isEqual` and if the result is true the selector will not be recomputed.
|
|
190
|
-
* If the result is true the re-render of the component will be prevented.
|
|
191
|
-
*/
|
|
192
|
-
isEqualRoot: (r1, r2) => r1.filter === r2.filter,
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* You can use the `isEqual` to validate if the values after the selector are equal.
|
|
196
|
-
* This validation will run after the selector computed a new value...
|
|
197
|
-
* and if the result is true it will prevent the re-render of the component.
|
|
198
|
-
*/
|
|
199
|
-
isEqual: (filter1, filter2) => filter1 === filter2,
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* You can use the `dependencies` array as with regular hooks to to force the recomputation of the selector.
|
|
203
|
-
* Is important ot mention that changes in the dependencies will not trigger a re-render of the component...
|
|
204
|
-
* Instead the recomputation of the selector will returned immediately.
|
|
205
|
-
*/
|
|
206
|
-
dependencies: [filter],
|
|
207
|
-
});
|
|
145
|
+
You can still **use dependencies** inside a selector hook:
|
|
208
146
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
))}
|
|
214
|
-
</ul>
|
|
147
|
+
```tsx
|
|
148
|
+
const [filteredContacts] = useContactsArray(
|
|
149
|
+
(contacts) => contacts.filter((c) => c.name.includes(filter)),
|
|
150
|
+
[filter]
|
|
215
151
|
);
|
|
216
152
|
```
|
|
217
153
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```TSX
|
|
221
|
-
({
|
|
222
|
-
/**
|
|
223
|
-
* You can use the `shallowCompare` from the GlobalStore.utils to compare the values at first level.
|
|
224
|
-
*/
|
|
225
|
-
isEqual: shallowCompare,
|
|
226
|
-
})
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Just remember, you can select or derive different values from the global state endlessly, but the state mutator will remain the same throughout the hooks.
|
|
230
|
-
|
|
231
|
-
More examples:
|
|
154
|
+
#### ✅ Selector hooks share the same state mutator
|
|
232
155
|
|
|
233
|
-
|
|
234
|
-
const useFilter = useContacts.createSelectorHook(({ filter }) => filter);
|
|
156
|
+
The **stateMutator remains the same** across all derived selectors, meaning actions and setState functions stay consistent.
|
|
235
157
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
158
|
+
```tsx
|
|
159
|
+
const [actions1] = useContactsArray();
|
|
160
|
+
const [actions2] = useContactsCount();
|
|
239
161
|
|
|
240
|
-
|
|
162
|
+
console.log(actions1 === actions2); // true
|
|
241
163
|
```
|
|
242
164
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
Each selector hook is reactive only to the fragment/derived of the state returned by the selector. And again you can optimize it by using the **isEqualRoot** and **isEqual** functions, which help avoid recomputing the selector if the root state or the fragment hasn't changed.
|
|
165
|
+
---
|
|
246
166
|
|
|
247
|
-
|
|
167
|
+
## 🎛 State Actions: Controlling State Modifications
|
|
248
168
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
By defining a custom API for the **useContacts**, we can encapsulate and expose only the necessary actions or operations that are allowed to modify the state. This provides a controlled interface for interacting with the state, ensuring that modifications stick to the desired restrictions.
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
import { createGlobalState } from "react-native-global-state-hooks";
|
|
169
|
+
Restrict **state modifications** by defining custom actions:
|
|
255
170
|
|
|
171
|
+
```tsx
|
|
256
172
|
export const useContacts = createGlobalState(
|
|
173
|
+
{ filter: "", items: [] },
|
|
257
174
|
{
|
|
258
|
-
isLoading: true,
|
|
259
|
-
filter: "",
|
|
260
|
-
items: [] as Contact[],
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
// this are the actions available for this state
|
|
264
175
|
actions: {
|
|
176
|
+
async fetch() {
|
|
177
|
+
return async ({ setState }) => {
|
|
178
|
+
const items = await fetchItems();
|
|
179
|
+
setState({ items });
|
|
180
|
+
};
|
|
181
|
+
},
|
|
265
182
|
setFilter(filter: string) {
|
|
266
183
|
return ({ setState }) => {
|
|
267
|
-
setState((state) => ({
|
|
268
|
-
...state,
|
|
269
|
-
filter,
|
|
270
|
-
}));
|
|
184
|
+
setState((state) => ({ ...state, filter }));
|
|
271
185
|
};
|
|
272
186
|
},
|
|
273
|
-
} as const,
|
|
274
|
-
onInit: ({ setState }) => {
|
|
275
|
-
// fetch contacts
|
|
276
187
|
},
|
|
277
188
|
}
|
|
278
189
|
);
|
|
279
190
|
```
|
|
280
191
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
Let's see how that will look now into our **FilterBar.tsx**
|
|
192
|
+
Now, instead of `setState`, the hook returns **actions**:
|
|
284
193
|
|
|
285
194
|
```tsx
|
|
286
|
-
const [
|
|
287
|
-
|
|
288
|
-
return <TextInput onChangeText={setFilter} />;
|
|
195
|
+
const [filter, { setFilter }] = useContacts();
|
|
289
196
|
```
|
|
290
197
|
|
|
291
|
-
|
|
198
|
+
---
|
|
292
199
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
```ts
|
|
296
|
-
const useFilter = createDerivate(useContacts, ({ filter }) => ({ filter }));
|
|
200
|
+
## 🌍 Accessing Global State Outside Components
|
|
297
201
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const useContacts = createDerivate(useContacts, ({ items }) => items);
|
|
301
|
-
|
|
302
|
-
const useContactsLength = createDerivate(useContacts, (items) => items.length);
|
|
303
|
-
|
|
304
|
-
const useIsContactsEmpty = createDerivate(useContactsLength, (length) => !length);
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
It can't get any simpler, right? Everything is connected, everything is reactive. Plus, these hooks are strongly typed, so if you're working with **TypeScript**, you'll absolutely love it.
|
|
308
|
-
|
|
309
|
-
# State Controls
|
|
310
|
-
|
|
311
|
-
If you need to access the global state outside of a component or a hook without subscribing to state changes, or even inside a **ClassComponent**, you can use:
|
|
202
|
+
Use `stateControls()` to **retrieve or update state outside React components**:
|
|
312
203
|
|
|
313
204
|
```tsx
|
|
314
|
-
useContacts.stateControls
|
|
315
|
-
|
|
316
|
-
// example:
|
|
317
|
-
const [getContacts, setContacts] = useContacts.stateControls();
|
|
318
|
-
|
|
319
|
-
console.log(getContacts()); // prints the list of contacts
|
|
205
|
+
const [contactsRetriever, contactsApi] = useContacts.stateControls();
|
|
206
|
+
console.log(contactsRetriever()); // Retrieves the current state
|
|
320
207
|
```
|
|
321
208
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
Using the **stateRetriever** and the **stateMutator** allows you to retrieve the state when needed without establishing a reactive relationship with the state changes. This approach provides more flexibility and control over when and how components interact with the global state.
|
|
325
|
-
|
|
326
|
-
So, While **useContacts** will allow your components to subscribe to the custom hook, using the **contactsRetriever** method you will be able retrieve the current value of the state. This allows you to access the state whenever necessary, without being reactive to its changes and with the **contactsMutator** you now have the ability to modify the state without the need for subscription to the hook.
|
|
209
|
+
#### ✅ Subscribe to changes
|
|
327
210
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* This not only allows you to retrieve the current value of the state...
|
|
333
|
-
* but also enables you to subscribe to any changes in the state or a portion of it
|
|
334
|
-
*/
|
|
335
|
-
const unsubscribe1 = contactsRetriever((state) => {
|
|
336
|
-
console.log("state changed: ", state);
|
|
211
|
+
```tsx
|
|
212
|
+
const unsubscribe = contactsRetriever((state) => {
|
|
213
|
+
console.log("State updated:", state);
|
|
337
214
|
});
|
|
338
|
-
|
|
339
|
-
const unsubscribe2 = contactsRetriever(
|
|
340
|
-
(state) => state.isLoading,
|
|
341
|
-
(isLoading) => {
|
|
342
|
-
console.log("is loading changed", isLoading);
|
|
343
|
-
}
|
|
344
|
-
);
|
|
345
215
|
```
|
|
346
216
|
|
|
347
|
-
|
|
217
|
+
#### ✅ Subscriptions are great when one state depends on another.
|
|
348
218
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
actions: {
|
|
360
|
-
log: (currentValue: string) => {
|
|
361
|
-
return ({ getState }: StoreTools<number>): void => {
|
|
362
|
-
console.log(`Current Value: ${getState()}`);
|
|
363
|
-
};
|
|
364
|
-
},
|
|
365
|
-
|
|
366
|
-
increase(value: number = 1) {
|
|
367
|
-
return ({ getState, setState, actions }: StoreTools<number>) => {
|
|
368
|
-
setState((count) => count + value);
|
|
369
|
-
|
|
370
|
-
actions.log(message);
|
|
371
|
-
};
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
decrease(value: number = 1) {
|
|
375
|
-
return ({ getState, setState, actions }: StoreTools<number>) => {
|
|
376
|
-
setState((count) => count - value);
|
|
377
|
-
|
|
378
|
-
actions.log(message);
|
|
379
|
-
};
|
|
219
|
+
```tsx
|
|
220
|
+
const useSelectedContact = createGlobalState(null, {
|
|
221
|
+
callbacks: {
|
|
222
|
+
onInit: ({ setState, getState }) => {
|
|
223
|
+
contactsRetriever(
|
|
224
|
+
(state) => state.contacts,
|
|
225
|
+
(contacts) => {
|
|
226
|
+
if (!contacts.has(getState())) setState(null);
|
|
227
|
+
}
|
|
228
|
+
);
|
|
380
229
|
},
|
|
381
|
-
}
|
|
230
|
+
},
|
|
382
231
|
});
|
|
383
232
|
```
|
|
384
233
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
# Stateful Context with Actions
|
|
388
|
-
|
|
389
|
-
**The ultimate blend of flexibility and control in React state management!** You can now create an isolated global state within a React context, giving each consumer of the context provider a unique state instance. But that’s not all...
|
|
234
|
+
---
|
|
390
235
|
|
|
391
|
-
|
|
236
|
+
## 🎭 Using Context for Scoped State
|
|
392
237
|
|
|
393
|
-
|
|
238
|
+
- **Scoped State** – Context state is **isolated inside the provider**.
|
|
239
|
+
- **Same API** – Context supports **selectors, actions, and state controls**.
|
|
394
240
|
|
|
395
|
-
|
|
241
|
+
### 📌 Creating a Context
|
|
396
242
|
|
|
397
243
|
```tsx
|
|
398
|
-
|
|
244
|
+
import { createContext } from "react-global-state-hooks/createContext";
|
|
245
|
+
export const [useCounterContext, CounterProvider] = createContext(0);
|
|
399
246
|
```
|
|
400
247
|
|
|
401
|
-
|
|
248
|
+
Wrap your app:
|
|
402
249
|
|
|
403
250
|
```tsx
|
|
404
251
|
<CounterProvider>
|
|
@@ -406,169 +253,131 @@ Then just wrap the components you need with the provider:
|
|
|
406
253
|
</CounterProvider>
|
|
407
254
|
```
|
|
408
255
|
|
|
409
|
-
|
|
256
|
+
Use the context state:
|
|
410
257
|
|
|
411
258
|
```tsx
|
|
412
|
-
const
|
|
413
|
-
|
|
259
|
+
const [count] = useCounterContext();
|
|
260
|
+
```
|
|
414
261
|
|
|
415
|
-
|
|
416
|
-
const [count, setCount] = useCounter();
|
|
262
|
+
### 📌 Context Selectors
|
|
417
263
|
|
|
418
|
-
|
|
419
|
-
};
|
|
420
|
-
```
|
|
264
|
+
Works **just like global state**, but within the provider.
|
|
421
265
|
|
|
422
|
-
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 🔥 Observables: Watching State Changes
|
|
269
|
+
|
|
270
|
+
Observables **let you react to state changes** via subscriptions.
|
|
271
|
+
|
|
272
|
+
### 📌 Creating an Observable
|
|
423
273
|
|
|
424
274
|
```tsx
|
|
425
|
-
const
|
|
426
|
-
|
|
275
|
+
export const useCounter = createGlobalState(0);
|
|
276
|
+
export const counterLogs = useCounter.createObservable((count) => `Counter is at ${count}`);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 📌 Subscribing to an Observable
|
|
427
280
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
};
|
|
281
|
+
```tsx
|
|
282
|
+
const unsubscribe = counterLogs((message) => {
|
|
283
|
+
console.log(message);
|
|
284
|
+
});
|
|
432
285
|
```
|
|
433
286
|
|
|
434
|
-
|
|
287
|
+
### 📌 Using Observables Inside Context
|
|
435
288
|
|
|
436
289
|
```tsx
|
|
437
|
-
const
|
|
438
|
-
|
|
290
|
+
export const [useStateControls, useObservableBuilder] = useCounterContext.stateControls();
|
|
291
|
+
const createObservable = useObservableBuilder();
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const unsubscribe = createObservable((count) => {
|
|
294
|
+
console.log(`Updated count: ${count}`);
|
|
295
|
+
});
|
|
296
|
+
return unsubscribe;
|
|
297
|
+
}, []);
|
|
298
|
+
```
|
|
439
299
|
|
|
440
|
-
|
|
441
|
-
const [isEven, setCount] = useCounter((count) => count % 2 === 0);
|
|
300
|
+
---
|
|
442
301
|
|
|
443
|
-
|
|
444
|
-
// Since the counter initially was 2 and now is 4, it’s still an even number.
|
|
445
|
-
// Because of this, the component will not re-render.
|
|
446
|
-
setCount(4);
|
|
447
|
-
}, []);
|
|
302
|
+
## ⚖️ `createGlobalState` vs. `createContext`
|
|
448
303
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
304
|
+
| Feature | `createGlobalState` | `createContext` |
|
|
305
|
+
| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
306
|
+
| **Scope** | Available globally across the entire app | Scoped to the Provider where it’s used |
|
|
307
|
+
| **How to Use** | `const useCount = createGlobalState(0)` | `const [useCountContext, Provider] = createContext(0)` |
|
|
308
|
+
| **createSelectorHook** | `useCount.createSelectorHook` | `useCountContext.createSelectorHook` |
|
|
309
|
+
| **inline selectors?** | ✅ Supported | ✅ Supported |
|
|
310
|
+
| **Custom Actions** | ✅ Supported | ✅ Supported |
|
|
311
|
+
| **Observables** | `useCount.createObservable` | `const [, useObservableBuilder] = useCountContext.stateControls()` |
|
|
312
|
+
| **State Controls** | `useCount.stateControls()` | `const [useStateControls] = useCountContext.stateControls()` |
|
|
313
|
+
| **Best For** | Global app state (auth, settings, cache) | Scoped module state, reusable component state, or state shared between child components without being fully global |
|
|
452
314
|
|
|
453
|
-
|
|
315
|
+
## 🔄 Lifecycle Methods
|
|
454
316
|
|
|
455
|
-
|
|
456
|
-
import { createStatefulContext, StoreTools } from "react-global-state-hooks";
|
|
317
|
+
Global state hooks support lifecycle callbacks for additional control.
|
|
457
318
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
},
|
|
319
|
+
```tsx
|
|
320
|
+
const useData = createGlobalState(
|
|
321
|
+
{ value: 1 },
|
|
462
322
|
{
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
setState((state) => ({
|
|
467
|
-
...state,
|
|
468
|
-
count: state.count + value,
|
|
469
|
-
}));
|
|
470
|
-
};
|
|
323
|
+
callbacks: {
|
|
324
|
+
onInit: ({ setState }) => {
|
|
325
|
+
console.log("Store initialized");
|
|
471
326
|
},
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}));
|
|
478
|
-
};
|
|
327
|
+
onStateChanged: ({ state, previousState }) => {
|
|
328
|
+
console.log("State changed:", previousState, "→", state);
|
|
329
|
+
},
|
|
330
|
+
computePreventStateChange: ({ state, previousState }) => {
|
|
331
|
+
return state.value === previousState.value;
|
|
479
332
|
},
|
|
480
|
-
}
|
|
333
|
+
},
|
|
481
334
|
}
|
|
482
335
|
);
|
|
483
336
|
```
|
|
484
337
|
|
|
485
|
-
|
|
338
|
+
Use **`onInit`** for setup, **`onStateChanged`** to listen to updates, and **`computePreventStateChange`** to prevent unnecessary updates.
|
|
486
339
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
340
|
+
## Metadata
|
|
341
|
+
|
|
342
|
+
There is a possibility to add non reactive information in the global state:
|
|
490
343
|
|
|
491
|
-
|
|
492
|
-
};
|
|
344
|
+
```tsx
|
|
345
|
+
const useCount = createGlobalState(0, { metadata: { renders: 0 } });
|
|
493
346
|
```
|
|
494
347
|
|
|
495
|
-
|
|
348
|
+
How to use it?
|
|
496
349
|
|
|
497
|
-
|
|
350
|
+
```tsx
|
|
351
|
+
const [count, , metadata] = useCount();
|
|
498
352
|
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* @description callback function called when the store is initialized
|
|
502
|
-
* @returns {void} result - void
|
|
503
|
-
* */
|
|
504
|
-
onInit?: ({
|
|
505
|
-
/**
|
|
506
|
-
* Set the metadata
|
|
507
|
-
* @param {TMetadata} setter - The metadata or a function that will receive the metadata and return the new metadata
|
|
508
|
-
* */
|
|
509
|
-
setMetadata: MetadataSetter<TMetadata>;
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Set the state
|
|
513
|
-
* @param {TState} setter - The state or a function that will receive the state and return the new state
|
|
514
|
-
* @param {{ forceUpdate?: boolean }} options - Options
|
|
515
|
-
* */
|
|
516
|
-
setState: StateSetter<TState>;
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Get the state
|
|
520
|
-
* @returns {TState} result - The state
|
|
521
|
-
* */
|
|
522
|
-
getState: () => TState;
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Get the metadata
|
|
526
|
-
* @returns {TMetadata} result - The metadata
|
|
527
|
-
* */
|
|
528
|
-
getMetadata: () => TMetadata;
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Actions of the hook if configuration was provided
|
|
532
|
-
*/
|
|
533
|
-
actions: TActions;
|
|
534
|
-
}: StateConfigCallbackParam<TState, TMetadata, TActions>) => void;
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* @description - callback function called every time the state is changed
|
|
538
|
-
*/
|
|
539
|
-
onStateChanged?: (parameters: StoreTools<any, any> & StateChanges<unknown>) => void;
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* callback function called every time a component is subscribed to the store
|
|
543
|
-
*/
|
|
544
|
-
onSubscribed?: (parameters: StoreTools<any, any>) => void;
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* callback function called every time the state is about to change and it allows you to prevent the state change
|
|
548
|
-
*/
|
|
549
|
-
computePreventStateChange?: (parameters: StoreTools<any, any> & StateChanges<unknown>) => boolean;
|
|
353
|
+
metadata.renders += 1;
|
|
550
354
|
```
|
|
551
355
|
|
|
552
|
-
|
|
356
|
+
## 🎯 Ready to Try It?
|
|
357
|
+
|
|
358
|
+
📦 **NPM Package:** [react-hooks-global-states](https://www.npmjs.com/package/react-hooks-global-states)
|
|
359
|
+
|
|
360
|
+
🚀 Simplify your **global state management** in React & React Native today! 🚀
|
|
361
|
+
|
|
362
|
+
# Using async persist storage
|
|
553
363
|
|
|
554
364
|
```ts
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
callbacks: {
|
|
562
|
-
// onSubscribed: (StateConfigCallbackParam) => {},
|
|
563
|
-
// onInit // etc
|
|
564
|
-
computePreventStateChange: ({ state, previousState }) => {
|
|
565
|
-
const prevent = isEqual(state, previousState);
|
|
365
|
+
const useCount = createGlobalState(0, {
|
|
366
|
+
asyncStorage: {
|
|
367
|
+
key: "count",
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
```
|
|
566
371
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
);
|
|
372
|
+
Inside your components
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
const [count, setCount, { isAsyncStorageReady }] = useCount();
|
|
572
376
|
```
|
|
573
377
|
|
|
574
|
-
|
|
378
|
+
- if you specify a key into the `asyncStorage` this will persist the state value if the same is serializable
|
|
379
|
+
- 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.
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
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;
|