react-hooks-global-states 6.0.6 → 6.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ The best part? **react-hooks-global-states** is compatible with both **React** a
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-hooks-global-states';
26
+ import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
27
27
 
28
28
  export const useCount = createGlobalState(0);
29
29
  ```
@@ -45,7 +45,7 @@ Isn't it cool? It works just like a regular **useState**. Notice the only differ
45
45
  What if you already have a global state that you want to subscribe to, but you don't want your component to listen to all the changes of the state, only a small portion of it? Let's create a more complex **state**
46
46
 
47
47
  ```ts
48
- import { createGlobalState } from 'react-hooks-global-states';
48
+ import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
49
49
 
50
50
  export const useContacts = createGlobalState({
51
51
  isLoading: true,
@@ -228,32 +228,29 @@ Is common and often necessary to restrict the manipulation of state to a specifi
228
228
  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.
229
229
 
230
230
  ```ts
231
- import { createGlobalState } from 'react-hooks-global-states';
232
-
233
- const initialState = {
234
- isLoading: true,
235
- filter: '',
236
- items: [] as Contact[],
237
- };
238
-
239
- type State = typeof initialState;
231
+ import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
240
232
 
241
233
  export const useContacts = createGlobalState(
242
- initialState,
243
234
  {
244
- onInit: async ({ setState }: StoreTools<State>) => {
245
- // fetch contacts
246
- },
235
+ isLoading: true,
236
+ filter: '',
237
+ items: [] as Contact[],
247
238
  },
248
- // this are the actions available for this state
249
239
  {
250
- setFilter(filter: string) {
251
- return ({ setState }: StoreTools<State>) => {
252
- setState((state) => ({
253
- ...state,
254
- filter,
255
- }));
256
- };
240
+ callbacks: {
241
+ onInit: ({ setState }) => {
242
+ // fetch contacts
243
+ },
244
+ actions: {
245
+ setFilter(filter: string) {
246
+ return ({ setState }) => {
247
+ setState((state) => ({
248
+ ...state,
249
+ filter,
250
+ }));
251
+ };
252
+ },
253
+ },
257
254
  },
258
255
  }
259
256
  );
@@ -276,7 +273,7 @@ Yeah, that's it! All the **derived states** and **emitters** (we will talk about
276
273
  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:
277
274
 
278
275
  ```tsx
279
- useContacts.stateControls: () => [stateRetriever: StateGetter<State>, stateMutator: Setter<State>|ActionCollectionResult<State>, metadataRetriever: Metadata];
276
+ useContacts.stateControls: () => [stateRetriever, stateMutator, metadataRetriever];
280
277
 
281
278
  // example:
282
279
  const [getContacts, setContacts] = useContacts.stateControls();
@@ -297,18 +294,16 @@ Additionally, to subscribe to state changes, you can pass a callback function as
297
294
  * This not only allows you to retrieve the current value of the state...
298
295
  * but also enables you to subscribe to any changes in the state or a portion of it
299
296
  */
300
- const removeSubscriptionGroup = contactsRetriever<Subscribe>((subscribe) => {
301
- subscribe((state) => {
302
- console.log('state changed: ', state);
303
- });
304
-
305
- subscribe(
306
- (state) => state.isLoading,
307
- (isLoading) => {
308
- console.log('is loading changed', isLoading);
309
- }
310
- );
297
+ const unsubscribe1 = contactsRetriever((state) => {
298
+ console.log('state changed: ', state);
311
299
  });
300
+
301
+ const unsubscribe2 = contactsRetriever(
302
+ (state) => state.isLoading,
303
+ (isLoading) => {
304
+ console.log('is loading changed', isLoading);
305
+ }
306
+ );
312
307
  ```
313
308
 
314
309
  That's great, isn't it? everything stays synchronized with the original state!!
@@ -320,31 +315,33 @@ Let's add more actions to the state and explore how to use one action from insid
320
315
  Here's an example of adding multiple actions to the state and utilizing one action within another:
321
316
 
322
317
  ```ts
323
- import { createGlobalState } from 'react-hooks-global-states';
318
+ import { createGlobalState } from 'react-hooks-global-states/createGlobalState';
324
319
 
325
- export const useCount = createGlobalState(0, () => ({
326
- log: (currentValue: string) => {
327
- return ({ getState }: StoreTools<number>): void => {
328
- console.log(`Current Value: ${getState()}`);
329
- };
330
- },
320
+ export const useCount = createGlobalState(0, {
321
+ actions: {
322
+ log: (currentValue: string) => {
323
+ return ({ getState }): void => {
324
+ console.log(`Current Value: ${getState()}`);
325
+ };
326
+ },
331
327
 
332
- increase(value: number = 1) {
333
- return ({ getState, setState, actions }: StoreTools<number>) => {
334
- setState((count) => count + value);
328
+ increase(value: number = 1) {
329
+ return ({ getState, setState, actions }) => {
330
+ setState((count) => count + value);
335
331
 
336
- actions.log(message);
337
- };
338
- },
332
+ actions.log(message);
333
+ };
334
+ },
339
335
 
340
- decrease(value: number = 1) {
341
- return ({ getState, setState, actions }: StoreTools<number>) => {
342
- setState((count) => count - value);
336
+ decrease(value: number = 1) {
337
+ return ({ getState, setState, actions }) => {
338
+ setState((count) => count - value);
343
339
 
344
- actions.log(message);
345
- };
340
+ actions.log(message);
341
+ };
342
+ },
346
343
  },
347
- }));
344
+ });
348
345
  ```
349
346
 
350
347
  Notice that the **StoreTools** will contain a reference to the generated actions API. From there, you'll be able to access all actions from inside another one... the **StoreTools** is generic and allow your to set an interface for getting the typing on the actions.
@@ -358,6 +355,8 @@ Notice that the **StoreTools** will contain a reference to the generated actions
358
355
  Forget about the boilerplate of creating a context... with **createContext** it's straightforward and powerful. You can create a context and provider with one line of code.
359
356
 
360
357
  ```tsx
358
+ import { createGlobalState } from 'react-hooks-global-states/createContext';
359
+
361
360
  export const [useCounterContext, CounterProvider] = createContext(2);
362
361
  ```
363
362
 
@@ -412,34 +411,31 @@ const MyComponent = () => {
412
411
  **createContext** also allows you to add custom actions to control the manipulation of the state inside the context
413
412
 
414
413
  ```tsx
415
- import { createContext } from 'react-global-state-hooks';
414
+ import { createContext } from 'react-global-state-hooks/createContext';
416
415
 
417
- type CounterState = {
418
- count: number;
419
- };
420
-
421
- const initialState: CounterState = {
422
- count: 0,
423
- };
424
-
425
- export const [useCounterContext, CounterProvider] = createContext(initialState, () => ({
426
- increase: (value: number = 1) => {
427
- return ({ setState }: StoreTools<CounterState>) => {
428
- setState((state) => ({
429
- ...state,
430
- count: state.count + value,
431
- }));
432
- };
433
- },
434
- decrease: (value: number = 1) => {
435
- return ({ setState }: StoreTools<CounterState>) => {
436
- setState((state) => ({
437
- ...state,
438
- count: state.count - value,
439
- }));
440
- };
441
- },
442
- }));
416
+ export const [useCounterContext, CounterProvider] = createContext(
417
+ { count: 0 },
418
+ {
419
+ actions: {
420
+ increase: (value: number = 1) => {
421
+ return ({ setState }) => {
422
+ setState((state) => ({
423
+ ...state,
424
+ count: state.count + value,
425
+ }));
426
+ };
427
+ },
428
+ decrease: (value: number = 1) => {
429
+ return ({ setState }) => {
430
+ setState((state) => ({
431
+ ...state,
432
+ count: state.count - value,
433
+ }));
434
+ };
435
+ },
436
+ },
437
+ }
438
+ );
443
439
  ```
444
440
 
445
441
  And just like with regular global hooks, now instead of a setState function, the hook will return the collection of actions
@@ -450,286 +446,6 @@ Last but not least, you can still creating **selectorHooks** with the **createSe
450
446
  const useIsEven = useCounterContext.createSelectorHook((count) => count % 2 === 0);
451
447
  ```
452
448
 
453
- # Emitters
454
-
455
- 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:
456
-
457
- ```ts
458
- const subscribeToFilter = createDerivateEmitter(contactsRetriever, ({ filter }) => ({
459
- filter,
460
- }));
461
- ```
462
-
463
- 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.
464
-
465
- Now we are able to add a callback that will be executed every time the state of the **filter** changes.
466
-
467
- ```ts
468
- const removeFilterSubscription = subscribeToFilter<Subscribe>(({ filter }) => {
469
- console.log(`The filter value changed: ${filter}`);
470
- });
471
- ```
472
-
473
- 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.
474
-
475
- ```ts
476
- const removeFilterSubscription = subscribeToFilter<Subscribe>(
477
- ({ filter }) => {
478
- console.log(`The filter value changed: ${filter}`);
479
- },
480
- {
481
- skipFirst: true,
482
- }
483
- );
484
- ```
485
-
486
- 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
487
-
488
- ```ts
489
- const removeFilterSubscription = subscribeToFilter<Subscribe>(
490
- ({ filter }) => filter,
491
- /**
492
- * Cause of the selector the filter now is an string
493
- */
494
- (filter) => {
495
- console.log(`The filter value changed: ${filter}`);
496
- },
497
- {
498
- skipFirst: true,
499
- /**
500
- * You can also override the default shallow comparison...
501
- * or disable it completely by setting the isEqual callback to null.
502
- */
503
- isEqual: (a, b) => a === b,
504
- // isEqual: null // this will avoid doing a shallow comparison
505
- }
506
- );
507
- ```
508
-
509
- 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:
510
-
511
- ```ts
512
- const subscribeToItems = createDerivateEmitter(contactsRetriever, ({ items }) => items);
513
-
514
- const subscribeToItemsLength = createDerivateEmitter(subscribeToItems, (items) => items.length);
515
- ```
516
-
517
- 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!
518
-
519
- # Combining stateRetriever
520
-
521
- 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**.
522
-
523
- 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:
524
-
525
- First we are gonna create a couple of **global states**, and extract the **stateRetriever**.
526
-
527
- ```ts
528
- const useHook1 = createGlobalState({
529
- propA: 1,
530
- propB: 2,
531
- });
532
-
533
- const [stateRetriever1, stateMutator1] = useHook1.stateControls();
534
-
535
- const useHook2 = createGlobalState({
536
- propC: 3,
537
- propD: 4,
538
- });
539
-
540
- const [, stateRetriever2] = useHook2.stateControls();
541
- ```
542
-
543
- Okay, cool, the first state as **propA, propB** while the second one has **propC, propD**, let's combine them:
544
-
545
- ```ts
546
- const [useCombinedHook, combinedStateRetriever] = combineAsyncGetters(
547
- {
548
- selector: ([state1, state2]) => ({
549
- ...state1,
550
- ...state2,
551
- }),
552
- },
553
- stateRetriever1,
554
- stateRetriever2
555
- );
556
- ```
557
-
558
- 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:
559
-
560
- ```ts
561
- const value = stateRetriever(); // { propA, propB, propC, propD }
562
-
563
- // subscribe to the new emitter
564
- const unsubscribeGroup = stateRetriever<Subscribe>((subscribe) => {
565
- subscribe((state) => {
566
- console.log(subscribe); // full state
567
- });
568
-
569
- // Please note that if you add a selector,
570
- // the callback will only trigger if the result of the selector changes.
571
- subscribe(
572
- ({ propA, propD }) => ({ propA, propD }),
573
- (derived) => {
574
- console.log(derived); // { propA, propD }
575
- }
576
- );
577
- });
578
- ```
579
-
580
- 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.
581
-
582
- ```ts
583
- const [combinedState] = useCombinedHook();
584
- ```
585
-
586
- 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.
587
-
588
- ### Let's explore some additional examples.
589
-
590
- 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.
591
-
592
- ```ts
593
- const [fragment] = useCombinedHook(({ propA, propD }) => ({ propA, propD }));
594
- ```
595
-
596
- 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.
597
-
598
- Let's see an example:
599
-
600
- ```ts
601
- const [useCombinedHook, combinedStateRetriever1] = combineAsyncGetters(
602
- {
603
- selector: ([state1, state2]) => ({
604
- ...state1,
605
- ...state2,
606
- }),
607
- },
608
- stateRetriever1,
609
- stateRetriever2
610
- );
611
-
612
- const useHook3 = createGlobalState({
613
- propE: 1,
614
- propF: 2,
615
- });
616
-
617
- const [stateRetriever3, stateMutator3] = useHook3.stateControls();
618
-
619
- const useIsLoading = createGlobalState(false);
620
-
621
- const [isLoadingStateRetriever, isLoadingMutator] = useIsLoading.stateControls();
622
- ```
623
-
624
- Once we created another peace of state, we can combine it with our other **global hooks** and **emitters**
625
-
626
- ```ts
627
- const [useCombinedHook2, combinedStateRetriever2] = combineAsyncGetters(
628
- {
629
- selector: ([state1, state2, isLoading]) => ({
630
- ...state1,
631
- ...state2,
632
- isLoading,
633
- }),
634
- },
635
- combinedStateRetriever1,
636
- stateRetriever3,
637
- isLoadingStateRetriever
638
- );
639
- ```
640
-
641
- 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.
642
-
643
- ### **Quick note**:
644
-
645
- Please be aware that the third parameter is a **dispose callback**, which can be particularly useful in **higher-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.
646
-
647
- # Extending Global Hooks
648
-
649
- Creating a global hook that connects to an asyncStorage is made incredibly easy with the **createCustomGlobalState** function.
650
-
651
- This function returns a new global state builder wrapped with the desired custom implementation, allowing you to get creative! Le'ts see and example:
652
-
653
- ```ts
654
- export const createGlobalState = createCustomGlobalState<
655
- {
656
- asyncStorageKey?: string;
657
- },
658
- {
659
- isAsyncStorageReady?: boolean;
660
- }
661
- >({
662
- onInitialize: async ({ setState, setMetadata }, config) => {
663
- setMetadata((metadata) => ({
664
- ...(metadata ?? {}),
665
- isAsyncStorageReady: undefined,
666
- }));
667
-
668
- const asyncStorageKey = config?.asyncStorageKey;
669
- if (!asyncStorageKey) return;
670
-
671
- const storedItem = (await asyncStorage.getItem(asyncStorageKey)) as string;
672
-
673
- setMetadata((metadata) => ({
674
- ...metadata,
675
- isAsyncStorageReady: true,
676
- }));
677
-
678
- if (storedItem === null) {
679
- return setState((state: unknown) => state, { forceUpdate: true });
680
- }
681
-
682
- const parsed = formatFromStore(storedItem, {
683
- jsonParse: true,
684
- });
685
-
686
- setState(parsed, { forceUpdate: true });
687
- },
688
-
689
- onChange: ({ getState }, config) => {
690
- if (!config?.asyncStorageKey) return;
691
-
692
- const state = getState();
693
-
694
- const formattedObject = formatToStore(state, {
695
- stringify: true,
696
- });
697
-
698
- asyncStorage.setItem(config.asyncStorageKey, formattedObject);
699
- },
700
- });
701
- ```
702
-
703
- 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.
704
-
705
- It is worth mentioning that the **onInitialize** function will be executed only once per global state.
706
-
707
- 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).
708
-
709
- Let's see how to create a global state using our new builder:
710
-
711
- ```ts
712
- const useTodos = createGlobalState(new Map<string, number>(), {
713
- config: {
714
- asyncStorageKey: 'todos',
715
- },
716
- });
717
- ```
718
-
719
- That's correct! If you add an **asyncStorageKey** to the state configuration, the state will be synchronized with the **asyncStorage**
720
-
721
- Let's see how to use this async storage hook into our components:
722
-
723
- ```ts
724
- const [todos, setTodos, {isAsyncStorageReady}] = useTodos();
725
-
726
- return (<>
727
- {isAsyncStorageReady ? <TodoList todos={todos} /> : <Text>Loading...</Text>}
728
- <>);
729
- ```
730
-
731
- The **metadata** is not reactive information and can only be modified from inside the global state lifecycle methods.
732
-
733
449
  # Life cycle methods
734
450
 
735
451
  There are some lifecycle methods available for use with global hooks, let's review them:
@@ -814,86 +530,7 @@ Finally, if you have a very specific necessity but still want to use the global
814
530
  Let's see an example again with the **asyncStorage** custom global hook but with the abstract class.
815
531
 
816
532
  ```ts
817
- export class GlobalStore<
818
- State,
819
- Metadata extends {
820
- isAsyncStorageReady?: boolean;
821
- },
822
- ActionsConfig extends ActionCollectionConfig<State, Metadata> | unknown
823
- > extends GlobalStoreAbstract<State, Metadata, ActionsConfig> {
824
- public asyncStorageKey?: string;
825
-
826
- constructor(
827
- state: State,
828
- args: {
829
- metadata?: Metadata;
830
- callbacks?: GlobalStoreCallbacks<State, Metadata>;
831
- actions?: ActionsConfig;
832
- name?: string;
833
- asyncStorageKey?: string;
834
- } = {}
835
- ) {
836
- super(state, args);
837
- this.asyncStorageKey = args.asyncStorageKey;
838
- this.initialize();
839
- }
840
-
841
- protected onInitialize = async ({
842
- setState,
843
- setMetadata,
844
- getState,
845
- }: StoreTools<State, Metadata>) => {
846
- if (!this.asyncStorageKey) return;
847
-
848
- const storedItem = (await asyncStorage.getItem(this.asyncStorageKey)) as string | null;
849
-
850
- setMetadata((metadata) => {
851
- ...metadata,
852
- isAsyncStorageReady: true,
853
- });
854
-
855
- if (storedItem === null) {
856
- const state = getState();
857
-
858
- // force the re-render of the subscribed components even if the state is the same
859
- return setState(state, { forceUpdate: true });
860
- }
861
-
862
- const items = formatFromStore<State>(storedItem, {
863
- jsonParse: true,
864
- });
865
-
866
- setState(items, { forceUpdate: true });
867
- };
868
-
869
- protected onChange = ({ getState }: StoreTools<State, Metadata> & StateChanges<State>) => {
870
- const asyncStorageKey = this.asyncStorageKey;
871
- if (!asyncStorageKey) return;
872
-
873
- const state = getState();
874
-
875
- const formattedObject = formatToStore(state, {
876
- stringify: true,
877
- });
878
-
879
- asyncStorage.setItem(asyncStorageKey, formattedObject);
880
- };
881
- }
882
- ```
883
-
884
- Then, from an instance of the global store, you will be able to access the hooks.
885
-
886
- ```ts
887
- const useCount = new GlobalStore(1, {
888
- config: {
889
- asyncStorageKey: 'counter',
890
- },
891
- }).getHook();
892
-
893
- const [stateRetriever, stateMutator, getMetadata] = useCount.stateControls();
894
-
895
- // into a component
896
- const [state, setState, { isAsyncStorageReady }] = useCount();
533
+ extends GlobalStoreAbstract<State, Metadata, ActionsConfig>
897
534
  ```
898
535
 
899
536
  # That's it for now!! hope you enjoy coding!!