react-native-global-state-hooks 7.1.0 → 8.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 +29 -0
- package/GlobalStore.js +1 -0
- package/GlobalStoreAbstract.d.ts +9 -0
- package/GlobalStoreAbstract.js +1 -0
- package/README.md +84 -537
- package/asyncStorageWrapper.d.ts +8 -0
- package/asyncStorageWrapper.js +1 -0
- package/bundle.js +1 -0
- package/combineRetrieverAsynchronously.d.ts +1 -0
- package/combineRetrieverAsynchronously.js +1 -0
- package/combineRetrieverEmitterAsynchronously.d.ts +1 -0
- package/combineRetrieverEmitterAsynchronously.js +1 -0
- package/createContext.d.ts +1 -0
- package/createContext.js +1 -0
- package/createCustomGlobalState.d.ts +22 -0
- package/createCustomGlobalState.js +1 -0
- package/createGlobalState.d.ts +20 -0
- package/createGlobalState.js +1 -0
- package/debounce.d.ts +1 -0
- package/debounce.js +1 -0
- package/getAsyncStorageItem.d.ts +2 -0
- package/getAsyncStorageItem.js +1 -0
- package/index.d.ts +23 -0
- package/isRecord.d.ts +1 -0
- package/isRecord.js +1 -0
- package/package.json +94 -8
- package/setAsyncStorageItem.d.ts +2 -0
- package/setAsyncStorageItem.js +1 -0
- package/shallowCompare.d.ts +1 -0
- package/shallowCompare.js +1 -0
- package/throwWrongKeyOnActionCollectionConfig.d.ts +1 -0
- package/throwWrongKeyOnActionCollectionConfig.js +1 -0
- package/types.d.ts +83 -0
- package/types.js +1 -0
- package/uniqueId.d.ts +1 -0
- package/uniqueId.js +1 -0
- package/uniqueSymbol.d.ts +1 -0
- package/uniqueSymbol.js +1 -0
- package/useConstantValueRef.d.ts +1 -0
- package/useConstantValueRef.js +1 -0
- package/webpack.config.js +89 -0
- package/lib/bundle.js +0 -2
- package/lib/bundle.js.LICENSE.txt +0 -1
- package/lib/src/GlobalStore.context.d.ts +0 -6
- package/lib/src/GlobalStore.d.ts +0 -31
- package/lib/src/GlobalStore.functionHooks.d.ts +0 -19
- package/lib/src/GlobalStore.types.d.ts +0 -124
- package/lib/src/GlobalStore.utils.d.ts +0 -16
- package/lib/src/GlobalStoreAbstract.d.ts +0 -16
- package/lib/src/index.d.ts +0 -12
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ The best part? [react-hooks-global-states](https://www.npmjs.com/package/react-h
|
|
|
23
23
|
We are gonna create a global state hook **useCount** with one line of code.
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
import { createGlobalState } from "react-native-global-state-hooks";
|
|
26
|
+
import { createGlobalState } from "react-native-global-state-hooks/createGlobalState";
|
|
27
27
|
|
|
28
28
|
export const useCount = createGlobalState(0);
|
|
29
29
|
```
|
|
@@ -118,8 +118,7 @@ Okay, everything works when the changes come from the state, but what happens if
|
|
|
118
118
|
const [filter, setFilter] = useState("");
|
|
119
119
|
|
|
120
120
|
const [contacts] = useContacts(
|
|
121
|
-
(state) =>
|
|
122
|
-
[...state.entities.values()].filter((item) => item.name.includes(filter)),
|
|
121
|
+
(state) => [...state.entities.values()].filter((item) => item.name.includes(filter)),
|
|
123
122
|
{
|
|
124
123
|
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
125
124
|
/**
|
|
@@ -140,12 +139,9 @@ export const useContacts = createGlobalState({
|
|
|
140
139
|
selected: Set<number>,
|
|
141
140
|
});
|
|
142
141
|
|
|
143
|
-
const useContactsArray = useContacts.createSelectorHook(
|
|
144
|
-
(
|
|
145
|
-
|
|
146
|
-
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
147
|
-
}
|
|
148
|
-
);
|
|
142
|
+
const useContactsArray = useContacts.createSelectorHook((state) => [...state.entities.values()], {
|
|
143
|
+
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
144
|
+
});
|
|
149
145
|
```
|
|
150
146
|
|
|
151
147
|
Now inside your component just call the new hook
|
|
@@ -155,39 +151,28 @@ Now inside your component just call the new hook
|
|
|
155
151
|
```tsx
|
|
156
152
|
const [filter, setFilter] = useState("");
|
|
157
153
|
|
|
158
|
-
const [contacts] = useContactsArray(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
dependencies: [filter],
|
|
162
|
-
}
|
|
163
|
-
);
|
|
154
|
+
const [contacts] = useContactsArray((entities) => entities.name.includes(filter), {
|
|
155
|
+
dependencies: [filter],
|
|
156
|
+
});
|
|
164
157
|
```
|
|
165
158
|
|
|
166
159
|
Or you can create another selectorHook from your **useContactsArray**
|
|
167
160
|
|
|
168
161
|
```ts
|
|
169
|
-
const useContactsArray = useContacts.createSelectorHook(
|
|
170
|
-
(
|
|
171
|
-
|
|
172
|
-
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
173
|
-
}
|
|
174
|
-
);
|
|
162
|
+
const useContactsArray = useContacts.createSelectorHook((state) => [...state.entities.values()], {
|
|
163
|
+
isEqualRoot: (a, b) => a.entities === b.entities,
|
|
164
|
+
});
|
|
175
165
|
|
|
176
|
-
const useContactsLength = useContactsArray.createSelectorHook(
|
|
177
|
-
(entities) => entities.length
|
|
178
|
-
);
|
|
166
|
+
const useContactsLength = useContactsArray.createSelectorHook((entities) => entities.length);
|
|
179
167
|
```
|
|
180
168
|
|
|
181
169
|
Or you can create a custom hook
|
|
182
170
|
|
|
183
171
|
```tsx
|
|
184
172
|
const useFilteredContacts = (filter: string) => {
|
|
185
|
-
const [contacts] = useContactsArray(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
dependencies: [filter],
|
|
189
|
-
}
|
|
190
|
-
);
|
|
173
|
+
const [contacts] = useContactsArray((entities) => entities.name.includes(filter), {
|
|
174
|
+
dependencies: [filter],
|
|
175
|
+
});
|
|
191
176
|
|
|
192
177
|
return contacts;
|
|
193
178
|
};
|
|
@@ -198,31 +183,28 @@ To summarize
|
|
|
198
183
|
```tsx
|
|
199
184
|
const [filter, setFilter] = useState("");
|
|
200
185
|
|
|
201
|
-
const [contacts] = useContacts(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
isEqualRoot: (r1, r2) => r1.filter === r2.filter,
|
|
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,
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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,
|
|
217
200
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
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
|
+
});
|
|
226
208
|
|
|
227
209
|
return (
|
|
228
210
|
<ul>
|
|
@@ -253,13 +235,9 @@ const useFilter = useContacts.createSelectorHook(({ filter }) => filter);
|
|
|
253
235
|
|
|
254
236
|
const useContactsArray = useContacts.createSelectorHook(({ items }) => items);
|
|
255
237
|
|
|
256
|
-
const useContactsLength = useContactsArray.createSelectorHook(
|
|
257
|
-
(items) => items.length
|
|
258
|
-
);
|
|
238
|
+
const useContactsLength = useContactsArray.createSelectorHook((items) => items.length);
|
|
259
239
|
|
|
260
|
-
const useIsContactsEmpty = useContactsLength.createSelectorHook(
|
|
261
|
-
(length) => !length
|
|
262
|
-
);
|
|
240
|
+
const useIsContactsEmpty = useContactsLength.createSelectorHook((length) => !length);
|
|
263
241
|
```
|
|
264
242
|
|
|
265
243
|
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.
|
|
@@ -275,30 +253,29 @@ By defining a custom API for the **useContacts**, we can encapsulate and expose
|
|
|
275
253
|
```ts
|
|
276
254
|
import { createGlobalState } from "react-native-global-state-hooks";
|
|
277
255
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
type State = typeof initialState;
|
|
285
|
-
|
|
286
|
-
export const useContacts = createGlobalState(initialState, {
|
|
287
|
-
// this are the actions available for this state
|
|
288
|
-
actions: {
|
|
289
|
-
setFilter(filter: string) {
|
|
290
|
-
return ({ setState }: StoreTools<State>) => {
|
|
291
|
-
setState((state) => ({
|
|
292
|
-
...state,
|
|
293
|
-
filter,
|
|
294
|
-
}));
|
|
295
|
-
};
|
|
296
|
-
},
|
|
297
|
-
} as const,
|
|
298
|
-
onInit: async ({ setState }: StoreTools<State>) => {
|
|
299
|
-
// fetch contacts
|
|
256
|
+
export const useContacts = createGlobalState(
|
|
257
|
+
{
|
|
258
|
+
isLoading: true,
|
|
259
|
+
filter: "",
|
|
260
|
+
items: [] as Contact[],
|
|
300
261
|
},
|
|
301
|
-
|
|
262
|
+
{
|
|
263
|
+
// this are the actions available for this state
|
|
264
|
+
actions: {
|
|
265
|
+
setFilter(filter: string) {
|
|
266
|
+
return ({ setState }) => {
|
|
267
|
+
setState((state) => ({
|
|
268
|
+
...state,
|
|
269
|
+
filter,
|
|
270
|
+
}));
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
} as const,
|
|
274
|
+
onInit: ({ setState }) => {
|
|
275
|
+
// fetch contacts
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
);
|
|
302
279
|
```
|
|
303
280
|
|
|
304
281
|
That's it! In this updated version, the **useContacts** hook will no longer return [**state**, **stateMutator**] but instead will return [**state**, **actions**]. This change will provide a more intuitive and convenient way to access and interact with the state and its associated actions.
|
|
@@ -355,18 +332,16 @@ Additionally, to subscribe to state changes, you can pass a callback function as
|
|
|
355
332
|
* This not only allows you to retrieve the current value of the state...
|
|
356
333
|
* but also enables you to subscribe to any changes in the state or a portion of it
|
|
357
334
|
*/
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
console.log("state changed: ", state);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
subscribe(
|
|
364
|
-
(state) => state.isLoading,
|
|
365
|
-
(isLoading) => {
|
|
366
|
-
console.log("is loading changed", isLoading);
|
|
367
|
-
}
|
|
368
|
-
);
|
|
335
|
+
const unsubscribe1 = contactsRetriever((state) => {
|
|
336
|
+
console.log("state changed: ", state);
|
|
369
337
|
});
|
|
338
|
+
|
|
339
|
+
const unsubscribe2 = contactsRetriever(
|
|
340
|
+
(state) => state.isLoading,
|
|
341
|
+
(isLoading) => {
|
|
342
|
+
console.log("is loading changed", isLoading);
|
|
343
|
+
}
|
|
344
|
+
);
|
|
370
345
|
```
|
|
371
346
|
|
|
372
347
|
That's great, isn't it? everything stays synchronized with the original state!!
|
|
@@ -452,9 +427,7 @@ const MyComponent = () => {
|
|
|
452
427
|
|
|
453
428
|
// This component can access only the stateMutator of the state,
|
|
454
429
|
// and won't re-render if the counter changes
|
|
455
|
-
return (
|
|
456
|
-
<button onClick={() => setCount((count) => count + 1)}>Increase</button>
|
|
457
|
-
);
|
|
430
|
+
return <button onClick={() => setCount((count) => count + 1)}>Increase</button>;
|
|
458
431
|
};
|
|
459
432
|
```
|
|
460
433
|
|
|
@@ -482,20 +455,14 @@ const MyComponent = () => {
|
|
|
482
455
|
```tsx
|
|
483
456
|
import { createStatefulContext, StoreTools } from "react-global-state-hooks";
|
|
484
457
|
|
|
485
|
-
type CounterState = {
|
|
486
|
-
count: number;
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const initialState: CounterState = {
|
|
490
|
-
count: 0,
|
|
491
|
-
};
|
|
492
|
-
|
|
493
458
|
export const [useCounterContext, CounterProvider] = createStatefulContext(
|
|
494
|
-
|
|
459
|
+
{
|
|
460
|
+
count: 0,
|
|
461
|
+
},
|
|
495
462
|
{
|
|
496
463
|
actions: {
|
|
497
464
|
increase: (value: number = 1) => {
|
|
498
|
-
return ({ setState }
|
|
465
|
+
return ({ setState }) => {
|
|
499
466
|
setState((state) => ({
|
|
500
467
|
...state,
|
|
501
468
|
count: state.count + value,
|
|
@@ -503,7 +470,7 @@ export const [useCounterContext, CounterProvider] = createStatefulContext(
|
|
|
503
470
|
};
|
|
504
471
|
},
|
|
505
472
|
decrease: (value: number = 1) => {
|
|
506
|
-
return ({ setState }
|
|
473
|
+
return ({ setState }) => {
|
|
507
474
|
setState((state) => ({
|
|
508
475
|
...state,
|
|
509
476
|
count: state.count - value,
|
|
@@ -525,332 +492,6 @@ const MyComponent = () => {
|
|
|
525
492
|
};
|
|
526
493
|
```
|
|
527
494
|
|
|
528
|
-
# Emitters
|
|
529
|
-
|
|
530
|
-
So, we have seen that we can subscribe a callback to state changes, create **selector hooks** from our global states. Guess what? We can also create derived **emitters** and subscribe callbacks to specific portions of the state. Let's review it:
|
|
531
|
-
|
|
532
|
-
```ts
|
|
533
|
-
const subscribeToFilter = createDerivateEmitter(
|
|
534
|
-
contactsRetriever,
|
|
535
|
-
({ filter }) => ({
|
|
536
|
-
filter,
|
|
537
|
-
})
|
|
538
|
-
);
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
Cool, it's basically the same, but instead of using the **hook** as a parameter, we just have to use the **stateRetriever** as a parameter, and that will make the magic.
|
|
542
|
-
|
|
543
|
-
Now we are able to add a callback that will be executed every time the state of the **filter** changes.
|
|
544
|
-
|
|
545
|
-
```ts
|
|
546
|
-
const removeFilterSubscription = subscribeToFilter<Subscribe>(({ filter }) => {
|
|
547
|
-
console.log(`The filter value changed: ${filter}`);
|
|
548
|
-
});
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
By default, the callback will be executed once subscribed, using the current value of the state. If you want to avoid this initial call, you can pass an extra parameter to the **subscribe** function.
|
|
552
|
-
|
|
553
|
-
```ts
|
|
554
|
-
const removeFilterSubscription = subscribeToFilter<Subscribe>(
|
|
555
|
-
({ filter }) => {
|
|
556
|
-
console.log(`The filter value changed: ${filter}`);
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
skipFirst: true,
|
|
560
|
-
}
|
|
561
|
-
);
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
Also, of course, if you have an exceptional case where you want to derived/selected directly from the current **emitter**, you can add a **selector**. This allows you to fine-tune the emitted values based on your requirements
|
|
565
|
-
|
|
566
|
-
```ts
|
|
567
|
-
const removeFilterSubscription = subscribeToFilter<Subscribe>(
|
|
568
|
-
({ filter }) => filter,
|
|
569
|
-
/**
|
|
570
|
-
* Cause of the selector the filter now is an string
|
|
571
|
-
*/
|
|
572
|
-
(filter) => {
|
|
573
|
-
console.log(`The filter value changed: ${filter}`);
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
skipFirst: true,
|
|
577
|
-
/**
|
|
578
|
-
* You can also override the default shallow comparison...
|
|
579
|
-
* or disable it completely by setting the isEqual callback to null.
|
|
580
|
-
*/
|
|
581
|
-
isEqual: (a, b) => a === b,
|
|
582
|
-
// isEqual: null // this will avoid doing a shallow comparison
|
|
583
|
-
}
|
|
584
|
-
);
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
And guess what again? You can also derive emitters from derived emitters without any trouble at all! It works basically the same. Let's see an example:
|
|
588
|
-
|
|
589
|
-
```ts
|
|
590
|
-
const subscribeToItems = createDerivateEmitter(
|
|
591
|
-
contactsRetriever,
|
|
592
|
-
({ items }) => items
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
const subscribeToItemsLength = createDerivateEmitter(
|
|
596
|
-
subscribeToItems,
|
|
597
|
-
(items) => items.length
|
|
598
|
-
);
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
The examples may seem a little silly, but they allow you to see the incredible things you can accomplish with these **derived states** and **emitters**. They open up a world of possibilities!
|
|
602
|
-
|
|
603
|
-
# Combining stateRetriever
|
|
604
|
-
|
|
605
|
-
What if you have two states and you want to combine them? You may have already guessed it right? ... you can create combined **emitters** and **hooks** from the hook **stateRetriever**.
|
|
606
|
-
|
|
607
|
-
By utilizing the approach of combining **emitters** and **hooks**, you can effectively merge multiple states and make them shareable. This allows for better organization and simplifies the management of the combined states. You don't need to refactor everything; you just need to combine the **global state hooks** you already have. Let's see a simple example:
|
|
608
|
-
|
|
609
|
-
First we are gonna create a couple of **global states**, and extract the **stateRetriever**.
|
|
610
|
-
|
|
611
|
-
```ts
|
|
612
|
-
const useHook1 = createGlobalState({
|
|
613
|
-
propA: 1,
|
|
614
|
-
propB: 2,
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
const [stateRetriever1, stateMutator1] = useHook1.stateControls();
|
|
618
|
-
|
|
619
|
-
const useHook2 = createGlobalState({
|
|
620
|
-
propC: 3,
|
|
621
|
-
propD: 4,
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const [, stateRetriever2] = useHook2.stateControls();
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
Okay, cool, the first state as **propA, propB** while the second one has **propC, propD**, let's combine them:
|
|
628
|
-
|
|
629
|
-
```ts
|
|
630
|
-
const [useCombinedHook, combinedStateRetriever] = combineAsyncGetters(
|
|
631
|
-
{
|
|
632
|
-
selector: ([state1, state2]) => ({
|
|
633
|
-
...state1,
|
|
634
|
-
...state2,
|
|
635
|
-
}),
|
|
636
|
-
},
|
|
637
|
-
stateRetriever1,
|
|
638
|
-
stateRetriever2
|
|
639
|
-
);
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
Well, that's it! Now you have access to a **combinedStateRetriever** that will return the combined value of the two states. From this new **combinedStateRetriever**, you can retrieve the value or subscribe to its changes. Let'see:
|
|
643
|
-
|
|
644
|
-
```ts
|
|
645
|
-
const value = stateRetriever(); // { propA, propB, propC, propD }
|
|
646
|
-
|
|
647
|
-
// subscribe to the new emitter
|
|
648
|
-
const unsubscribeGroup = stateRetriever<Subscribe>((subscribe) => {
|
|
649
|
-
subscribe((state) => {
|
|
650
|
-
console.log(subscribe); // full state
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
// Please note that if you add a selector,
|
|
654
|
-
// the callback will only trigger if the result of the selector changes.
|
|
655
|
-
subscribe(
|
|
656
|
-
({ propA, propD }) => ({ propA, propD }),
|
|
657
|
-
(derived) => {
|
|
658
|
-
console.log(derived); // { propA, propD }
|
|
659
|
-
}
|
|
660
|
-
);
|
|
661
|
-
});
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
Regarding the newly created hook, **useCombinedHook**, you can seamlessly utilize it across all your components, just like your other **global state hooks**. This enables a consistent and familiar approach for accessing and managing the combined state within your application.
|
|
665
|
-
|
|
666
|
-
```ts
|
|
667
|
-
const [combinedState] = useCombinedHook();
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
The main difference with **combined hooks** compared to individual **global state hooks** is the absence of **metadata** and **actions**. Instead, combined hooks provide a condensed representation of the underlying global states using simple React functionality. This streamlined approach ensures lightweight usage, making it easy to access and manage the combined state within your components.
|
|
671
|
-
|
|
672
|
-
### Let's explore some additional examples.
|
|
673
|
-
|
|
674
|
-
Similar to your other **global state hooks**, **combined hooks** allow you to use **selectors** directly from consumer components. This capability eliminates the need to create an excessive number of reusable hooks if they are not truly necessary. By utilizing selectors, you can efficiently extract specific data from the **combined state** and utilize it within your components. This approach offers a more concise and focused way of accessing the required state values without the need for creating additional hooks unnecessarily.
|
|
675
|
-
|
|
676
|
-
```ts
|
|
677
|
-
const [fragment] = useCombinedHook(({ propA, propD }) => ({ propA, propD }));
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
Lastly, you have the flexibility to continue combining stateRetrievers if desired. This means you can extend the functionality of combined hooks by adding more stateRetrievers to merge additional states. By combining stateRetrievers in this way, you can create a comprehensive and unified representation of the combined states within your application. This approach allows for modular and scalable state management, enabling you to efficiently handle complex state compositions.
|
|
681
|
-
|
|
682
|
-
Let's see an example:
|
|
683
|
-
|
|
684
|
-
```ts
|
|
685
|
-
const [useCombinedHook, combinedStateRetriever1] = combineAsyncGetters(
|
|
686
|
-
{
|
|
687
|
-
selector: ([state1, state2]) => ({
|
|
688
|
-
...state1,
|
|
689
|
-
...state2,
|
|
690
|
-
}),
|
|
691
|
-
},
|
|
692
|
-
stateRetriever1,
|
|
693
|
-
stateRetriever2
|
|
694
|
-
);
|
|
695
|
-
|
|
696
|
-
const useHook3 = createGlobalState({
|
|
697
|
-
propE: 1,
|
|
698
|
-
propF: 2,
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
const [stateRetriever3, stateMutator3] = useHook3.stateControls();
|
|
702
|
-
|
|
703
|
-
const useIsLoading = createGlobalState(false);
|
|
704
|
-
|
|
705
|
-
const [isLoadingStateRetriever, isLoadingMutator] =
|
|
706
|
-
useIsLoading.stateControls();
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
Once we created another peace of state, we can combine it with our other **global hooks** and **emitters**
|
|
710
|
-
|
|
711
|
-
```ts
|
|
712
|
-
const [useCombinedHook2, combinedStateRetriever2] = combineAsyncGetters(
|
|
713
|
-
{
|
|
714
|
-
selector: ([state1, state2, isLoading]) => ({
|
|
715
|
-
...state1,
|
|
716
|
-
...state2,
|
|
717
|
-
isLoading,
|
|
718
|
-
}),
|
|
719
|
-
},
|
|
720
|
-
combinedStateRetriever1,
|
|
721
|
-
stateRetriever3,
|
|
722
|
-
isLoadingStateRetriever
|
|
723
|
-
);
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
You have the freedom to combine as many global hooks as you wish. This means you can merge multiple states into a single cohesive unit by combining their respective hooks. This approach offers flexibility and scalability, allowing you to handle complex state compositions in a modular and efficient manner.
|
|
727
|
-
|
|
728
|
-
### **Quick note**:
|
|
729
|
-
|
|
730
|
-
Please be aware that the third parameter is a **dispose callback**, which can be particularly useful in **high-order** functions when you want to release any resources associated with the hook. By invoking the dispose callback, the hook will no longer report any changes, ensuring that resources are properly cleaned up. This allows for efficient resource management and can be beneficial in scenarios where you need to handle resource cleanup or termination in a controlled manner.
|
|
731
|
-
|
|
732
|
-
# Extending Global Hooks
|
|
733
|
-
|
|
734
|
-
## `[IMPORTANT!]`: From version 6.0.0, you can continue creating your custom implementations or using your previous ones. However, now AsyncStorage is already integrated into the global hooks with @react-native-async-storage/async-storage. You simply need to add a key for the persistent storage, and that will do the trick.
|
|
735
|
-
|
|
736
|
-
**`BUT!`** If you are against using **@react-native-async-storage/async-storage**, you can always use the base library [**react-hooks-global-state**](https://www.npmjs.com/package/react-hooks-global-states), which provides all the global hooks functionality without the persistent storage options. Take a look at the documentation—there's even an example of how to implement your own persistent storage in your own way.
|
|
737
|
-
|
|
738
|
-
```ts
|
|
739
|
-
// this is all you need fo using async storage
|
|
740
|
-
const useCountPersisted = createGlobalState(1, {
|
|
741
|
-
asyncStorage: {
|
|
742
|
-
key: "count",
|
|
743
|
-
},
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* Usage in your components:
|
|
748
|
-
* [NOTE]: If no key is provided, the default metadata is null. Otherwise, it's set to { isAsyncStorageReady: false }.
|
|
749
|
-
*
|
|
750
|
-
* Upon the first successful retrieval from AsyncStorage, components will re-render with { isAsyncStorageReady: true } in the metadata.
|
|
751
|
-
* 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
|
|
752
|
-
*/
|
|
753
|
-
const [count, setCount, { isAsyncStorageReady }] = useCountPersisted();
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
##### Now lets continue analyzing how to create a custom GlobalStore!
|
|
757
|
-
|
|
758
|
-
Creating a global hook that connects to an asyncStorage is made incredibly easy with the **createCustomGlobalState** function.
|
|
759
|
-
|
|
760
|
-
This function returns a new global state builder wrapped with the desired custom implementation, allowing you to get creative! Le'ts see and example:
|
|
761
|
-
|
|
762
|
-
```ts
|
|
763
|
-
import { formatFromStore, formatToStore, createCustomGlobalState } = 'react-native-global-state-hooks'
|
|
764
|
-
|
|
765
|
-
// Optional configuration available for the consumers of the builder
|
|
766
|
-
type HookConfig = {
|
|
767
|
-
asyncStorageKey?: string;
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
// This is the base metadata that all the stores created from the builder will have.
|
|
771
|
-
type BaseMetadata = {
|
|
772
|
-
isAsyncStorageReady?: boolean;
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
export const createGlobalState = createCustomGlobalState<
|
|
776
|
-
BaseMetadata,
|
|
777
|
-
HookConfig
|
|
778
|
-
>({
|
|
779
|
-
/**
|
|
780
|
-
* This function executes immediately after the global state is created, before the invocations of the hook
|
|
781
|
-
*/
|
|
782
|
-
onInitialize: async ({ setState, setMetadata }, config) => {
|
|
783
|
-
setMetadata((metadata) => ({
|
|
784
|
-
...(metadata ?? {}),
|
|
785
|
-
isAsyncStorageReady: null,
|
|
786
|
-
}));
|
|
787
|
-
|
|
788
|
-
const asyncStorageKey = config?.asyncStorageKey;
|
|
789
|
-
if (!asyncStorageKey) return;
|
|
790
|
-
|
|
791
|
-
const storedItem = (await asyncStorage.getItem(asyncStorageKey)) as string;
|
|
792
|
-
|
|
793
|
-
// update the metadata, remember, metadata is not reactive
|
|
794
|
-
setMetadata((metadata) => ({
|
|
795
|
-
...metadata,
|
|
796
|
-
isAsyncStorageReady: true,
|
|
797
|
-
}));
|
|
798
|
-
|
|
799
|
-
if (storedItem === null) {
|
|
800
|
-
return setState((state) => state, { forceUpdate: true });
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const parsed = formatFromStore(storedItem, {
|
|
804
|
-
jsonParse: true,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
setState(parsed, { forceUpdate: true });
|
|
808
|
-
},
|
|
809
|
-
|
|
810
|
-
onChange: ({ getState }, config) => {
|
|
811
|
-
if (!config?.asyncStorageKey) return;
|
|
812
|
-
|
|
813
|
-
const state = getState();
|
|
814
|
-
|
|
815
|
-
const formattedObject = formatToStore(state, {
|
|
816
|
-
stringify: true,
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
asyncStorage.setItem(config.asyncStorageKey, formattedObject);
|
|
820
|
-
},
|
|
821
|
-
});
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
It is important to use **forceUpdate** to force React to re-render our components and obtain the most recent state of the **metadata**. This is especially useful when working with primitive types, as it can be challenging to differentiate between a primitive value that originates from storage and one that does not.
|
|
825
|
-
|
|
826
|
-
It is worth mentioning that the **onInitialize** function will be executed only once per global state.
|
|
827
|
-
|
|
828
|
-
You can use to **formatToStore**, and **formatFromStore** to sanitize your data, These methods will help you transform objects into JSON strings and retrieve them back without losing any of the original data types. You will no longer encounter problems when **stringifying** Dates, Maps, Sets, and other complex data types. You could take a look in the API here: [json-storage-formatter](https://www.npmjs.com/package/json-storage-formatter).
|
|
829
|
-
|
|
830
|
-
Let's see how to create a global state using our new builder:
|
|
831
|
-
|
|
832
|
-
```ts
|
|
833
|
-
const useTodos = createGlobalState(new Map<string, number>(), {
|
|
834
|
-
config: {
|
|
835
|
-
asyncStorageKey: "todos",
|
|
836
|
-
},
|
|
837
|
-
});
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
That's correct! If you add an **asyncStorageKey** to the state configuration, the state will be synchronized with the **asyncStorage**
|
|
841
|
-
|
|
842
|
-
Let's see how to use this async storage hook into our components:
|
|
843
|
-
|
|
844
|
-
```ts
|
|
845
|
-
const [todos, setTodos, metadata] = useTodos();
|
|
846
|
-
|
|
847
|
-
return (<>
|
|
848
|
-
{metadata.isAsyncStorageReady ? <TodoList todos={todos} /> : <Text>Loading...</Text>}
|
|
849
|
-
<>);
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
The **metadata** is not reactive information and can only be modified from inside the global state lifecycle methods.
|
|
853
|
-
|
|
854
495
|
# Life cycle methods
|
|
855
496
|
|
|
856
497
|
There are some lifecycle methods available for use with global hooks, let's review them:
|
|
@@ -895,17 +536,17 @@ onInit?: ({
|
|
|
895
536
|
/**
|
|
896
537
|
* @description - callback function called every time the state is changed
|
|
897
538
|
*/
|
|
898
|
-
onStateChanged?: (parameters:
|
|
539
|
+
onStateChanged?: (parameters: StoreTools<any, any> & StateChanges<unknown>) => void;
|
|
899
540
|
|
|
900
541
|
/**
|
|
901
542
|
* callback function called every time a component is subscribed to the store
|
|
902
543
|
*/
|
|
903
|
-
onSubscribed?: (parameters:
|
|
544
|
+
onSubscribed?: (parameters: StoreTools<any, any>) => void;
|
|
904
545
|
|
|
905
546
|
/**
|
|
906
547
|
* callback function called every time the state is about to change and it allows you to prevent the state change
|
|
907
548
|
*/
|
|
908
|
-
computePreventStateChange?: (parameters:
|
|
549
|
+
computePreventStateChange?: (parameters: StoreTools<any, any> & StateChanges<unknown>) => boolean;
|
|
909
550
|
```
|
|
910
551
|
|
|
911
552
|
You can pass this callbacks between on the second parameter of the builders like **createGlobalState**
|
|
@@ -917,111 +558,17 @@ const useData = createGlobalState(
|
|
|
917
558
|
metadata: {
|
|
918
559
|
someExtraInformation: "someExtraInformation",
|
|
919
560
|
},
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
561
|
+
callbacks: {
|
|
562
|
+
// onSubscribed: (StateConfigCallbackParam) => {},
|
|
563
|
+
// onInit // etc
|
|
564
|
+
computePreventStateChange: ({ state, previousState }) => {
|
|
565
|
+
const prevent = isEqual(state, previousState);
|
|
924
566
|
|
|
925
|
-
|
|
567
|
+
return prevent;
|
|
568
|
+
},
|
|
926
569
|
},
|
|
927
570
|
}
|
|
928
571
|
);
|
|
929
572
|
```
|
|
930
573
|
|
|
931
|
-
Finally, if you have a very specific necessity but still want to use the global hooks, you can extend the **GlobalStoreAbstract** class. This will give you even more control over the state and the lifecycle of the global state.
|
|
932
|
-
|
|
933
|
-
Let's see an example again with the **asyncStorage** custom global hook but with the abstract class.
|
|
934
|
-
|
|
935
|
-
```ts
|
|
936
|
-
export class GlobalStore<
|
|
937
|
-
TState,
|
|
938
|
-
TMetadata extends {
|
|
939
|
-
asyncStorageKey?: string;
|
|
940
|
-
isAsyncStorageReady?: boolean;
|
|
941
|
-
} | null = null,
|
|
942
|
-
TStateSetter extends
|
|
943
|
-
| ActionCollectionConfig<TState, TMetadata>
|
|
944
|
-
| StateSetter<TState> = StateSetter<TState>
|
|
945
|
-
> extends GlobalStoreAbstract<TState, TMetadata, TStateSetter> {
|
|
946
|
-
constructor(
|
|
947
|
-
state: TState,
|
|
948
|
-
config: GlobalStoreConfig<TState, TMetadata, TStateSetter> = {},
|
|
949
|
-
actionsConfig: TStateSetter | null = null
|
|
950
|
-
) {
|
|
951
|
-
super(state, config, actionsConfig);
|
|
952
|
-
|
|
953
|
-
this.initialize();
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
protected onInitialize = async ({
|
|
957
|
-
setState,
|
|
958
|
-
setMetadata,
|
|
959
|
-
getMetadata,
|
|
960
|
-
getState,
|
|
961
|
-
}: StateConfigCallbackParam<TState, TMetadata, TStateSetter>) => {
|
|
962
|
-
setMetadata({
|
|
963
|
-
...(metadata ?? {}),
|
|
964
|
-
isAsyncStorageReady: null,
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
const metadata = getMetadata();
|
|
968
|
-
const asyncStorageKey = metadata?.asyncStorageKey;
|
|
969
|
-
|
|
970
|
-
if (!asyncStorageKey) return;
|
|
971
|
-
|
|
972
|
-
const storedItem = (await asyncStorage.getItem(asyncStorageKey)) as string;
|
|
973
|
-
setMetadata({
|
|
974
|
-
...metadata,
|
|
975
|
-
isAsyncStorageReady: true,
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
if (storedItem === null) {
|
|
979
|
-
const state = getState();
|
|
980
|
-
|
|
981
|
-
// force the re-render of the subscribed components even if the state is the same
|
|
982
|
-
return setState(state, { forceUpdate: true });
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const items = formatFromStore<TState>(storedItem, {
|
|
986
|
-
jsonParse: true,
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
setState(items, { forceUpdate: true });
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
protected onChange = ({
|
|
993
|
-
getMetadata,
|
|
994
|
-
getState,
|
|
995
|
-
}: StateChangesParam<TState, TMetadata, NonNullable<TStateSetter>>) => {
|
|
996
|
-
const asyncStorageKey = getMetadata()?.asyncStorageKey;
|
|
997
|
-
|
|
998
|
-
if (!asyncStorageKey) return;
|
|
999
|
-
|
|
1000
|
-
const state = getState();
|
|
1001
|
-
|
|
1002
|
-
const formattedObject = formatToStore(state, {
|
|
1003
|
-
stringify: true,
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
asyncStorage.setItem(asyncStorageKey, formattedObject);
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
Then, from an instance of the global store, you will be able to access the hooks.
|
|
1012
|
-
|
|
1013
|
-
```ts
|
|
1014
|
-
const storage = new GlobalStore(0, {
|
|
1015
|
-
metadata: {
|
|
1016
|
-
asyncStorageKey: "counter",
|
|
1017
|
-
isAsyncStorageReady: false,
|
|
1018
|
-
},
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
const [getState, _, getMetadata] = storage.getHookDecoupled();
|
|
1022
|
-
const useState = storage.getHook();
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
### **Note**: The GlobalStore class is still available in the package in case you were already extending from it.
|
|
1026
|
-
|
|
1027
574
|
# That's it for now!! hope you enjoy coding!!
|