react-native-global-state-hooks 3.0.6 → 3.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
@@ -1,164 +1,342 @@
1
1
  # react-native-global-state-hooks
2
2
 
3
- This is a package to easily handling global-state across your react-native-components
4
-
5
- This utility uses the **useState** hook within a subscription pattern and **HOFs** to create a more intuitive, atomic and easy way of sharing state between components... You can see the introduction video [here!](https://www.youtube.com/watch?v=WfoMhO1zZ04&t=8s)
6
-
7
- To see TODO-LIST with the hooks and async storage take a look here: [todo-list-with-global-hooks](https://github.com/johnny-quesada-developer/todo-list-with-global-hooks.git)
3
+ This is a package to easily handling **global state hooks** across your **react components**
8
4
 
9
5
  For seen a running example of the hooks, you can check the following link: [react-global-state-hooks-example](https://johnny-quesada-developer.github.io/react-global-state-hooks-example/)
10
6
 
11
- ...
7
+ To see **TODO-LIST** with the hooks and **async storage** take a look here: [todo-list-with-global-hooks](https://github.com/johnny-quesada-developer/todo-list-with-global-hooks.git).
8
+
9
+ To see how to create a custom hook connected to your favorite async storage, please refer to the documentation section titled **Extending Global Hooks**
12
10
 
13
- ...
11
+ You can also see an introduction video [here!](https://www.youtube.com/watch?v=WfoMhO1zZ04&t=8s)
14
12
 
15
- # Creating a global store
13
+ # Creating a global state
16
14
 
17
- We are gonna create a global count example **useCountGlobal.ts**:
15
+ We are gonna create a global state hook **useCount** with one line of code.
18
16
 
19
17
  ```ts
20
- import { GlobalStore } from 'react-native-global-state-hooks';
18
+ import { createGlobalState } from 'react-native-global-state-hooks';
19
+
20
+ export const useCount = createGlobalState(0);
21
+ ```
21
22
 
22
- // initialize your store with the default value of the same.
23
- const countStore = new GlobalStore(0);
23
+ That's it! Welcome to global hooks. Now, you can use this state wherever you need it in your application.
24
24
 
25
- // get the hook
26
- export const useCountGlobal = countStore.getHook();
25
+ Let's see how to use it inside a simple **component**
27
26
 
28
- // inside your component just call...
29
- const [count, setCount] = useCountGlobal(); // no paremeters are needed since this is a global store
27
+ ```ts
28
+ const [count, setCount] = useCount();
29
+
30
+ return <Button onClick={() => setCount((count) => count + 1)}>{count}</Button>;
31
+ ```
30
32
 
31
- // That's it, that's a global store... Strongly typed, with a global-hook that we could reuse cross all our react-components.
33
+ Isn't it cool? It works just like a regular **useState**. Notice the only difference is that now you don't need to provide the initial value since this is a global hook, and the initial value has already been provided.
32
34
 
33
- // #### Optionally you are able to use a decoupled hook,
34
- // #### This function is linked to the store hooks but is not a hook himself.
35
+ # Decoupled state access
35
36
 
36
- export const [getCount, sendCount] = countStore.getHookDecoupled();
37
+ If you need to access the global state outside of a component or a hook without subscribing to state changes, you can use the **createGlobalStateWithDecoupledFuncs**.
37
38
 
38
- // @example
39
- console.log(getCount()); // 0;
39
+ Decoupled state access is particularly useful when you want to create components that have editing access to a specific store but don't necessarily need to reactively respond to state changes. For example, consider a search component that only needs to retrieve the current state whenever it performs a data search. It doesn't require continuous subscription to changes in the collection it filters.
40
40
 
41
- // components subscribed to the global hook if there are so
42
- sendCount(5);
41
+ Using decoupled state access 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. Let's see and example:
42
+
43
+ ```ts
44
+ import { createGlobalStateWithDecoupledFuncs } from 'react-native-global-state-hooks';
43
45
 
44
- console.log(getCount()); // 5;
46
+ export const [useCount, getCount, setCount] =
47
+ createGlobalStateWithDecoupledFuncs(0);
45
48
  ```
46
49
 
47
- ...
50
+ That's great! With the addition of the **getCount** and **setCount** methods, you now have the ability to access and modify the state without the need for subscription to the hook.
48
51
 
49
- ...
52
+ By using the **getCount** method, you can retrieve the current value of the state stored in **useCount**. This allows you to access the state whenever necessary, without being reactive to its changes.
50
53
 
51
- # Implementing your global hook into your components
54
+ Similarly, the **setCount** method enables you to modify the state stored in **useCount**. You can use this method to update the state with a new value or perform any necessary state mutations.
52
55
 
53
- Let's say we have two components **MyFirstComponent**, **MySecondComponent**, in order to use our global hook they will look just like:
56
+ These additional methods provide a more flexible and granular way to interact with the state managed by **useCount**. You can retrieve and modify the state as needed, without establishing a subscription relationship or reactivity with the state changes.
54
57
 
55
- ```JSX
56
- import { useCountGlobal } from './useCountGlobal'
58
+ # State actions
57
59
 
58
- const MyFirstComponent: React.FC = () => {
59
- const [count, setter] = useCountGlobal();
60
- const onClickAddOne = () => setter(count + 1);
60
+ Is common and often necessary to restrict the manipulation of state to a specific set of actions or operations. To achieve this, we can simplify the process by adding a custom API to the configuration of our **global state**.
61
61
 
62
- return (<Button title={`count: ${count}`} onPress={onClickAddOne} />);
63
- }
62
+ By defining a custom API for the **useCount**, 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 adhere to the desired restrictions.
64
63
 
65
- const MySecondComponent: React.FC = () => {
66
- const [count, setter] = useCountGlobal();
64
+ Let's see and example (you can use **createGlobalStateWithDecoupledFuncs** or **createGlobalState**, but methods work the the same)
67
65
 
68
- // it can also be use as a normal setter into a callback or other hooks
69
- const onClickAddTwo = useCallback(() => setter(state => state + 2), [])
66
+ ```ts
67
+ import { createGlobalState } from 'react-native-global-state-hooks';
70
68
 
71
- return (<Button title={`count: ${count}`} onPress={onClickAddOne} />);
72
- }
69
+ export const useCount = createGlobalState(0, {
70
+ actions: {
71
+ increase(value: number = 1) {
72
+ return ({ getState }: StoreTools<number>) => {
73
+ setState((count) => count + value);
74
+ };
75
+ },
76
+ },
77
+ });
78
+ ```
79
+
80
+ That's great! In this updated version, the **useCount** hook will no longer return [**state**, **stateSetter**] 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.
81
+
82
+ Let's see how that will look into a react component:
73
83
 
74
- // It's so simple to share information between components
84
+ ```ts
85
+ const [count, actions] = useCount();
86
+
87
+ // Every time we click, the counter will increase by 2
88
+ return <Button onClick={() => actions.increase(2)}>{count}</Button>;
75
89
  ```
76
90
 
77
- Note that the only difference between this and the default **useState** hook is that you are not adding the initial value, cause you already did that when you created the store:
91
+ Let's add more actions to the state and explore how to use one action from inside another.
92
+
93
+ Here's an example of adding multiple actions to the state and utilizing one action within another:
78
94
 
79
95
  ```ts
80
- const countStore = new GlobalStore(0);
96
+ import { createGlobalState } from 'react-native-global-state-hooks';
97
+
98
+ export const useCount = createGlobalState(0, {
99
+ actions: {
100
+ log: (currentValue: string) => {
101
+ return ({ getState }: StoreTools<number>): void => {
102
+ console.log(`Current Value: ${getState()}`);
103
+ };
104
+ },
105
+
106
+ increase(value: number = 1) {
107
+ return ({ getState, setState, actions }: StoreTools<number>) => {
108
+ setState((count) => count + value);
109
+
110
+ actions.log(message);
111
+ };
112
+ },
113
+
114
+ decrease(value: number = 1) {
115
+ return ({ getState, setState, actions }: StoreTools<number>) => {
116
+ setState((count) => count - value);
117
+
118
+ actions.log(message);
119
+ };
120
+ },
121
+ },
122
+ });
81
123
  ```
82
124
 
83
- ...
125
+ 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.
126
+
127
+ If you don't want to create an extra type please use **createGlobalStateWithDecoupledFuncs** in that way you'll be able to use the decoupled **actions** which will have the correct typing. Let's take a quick look into that:
128
+
129
+ ```ts
130
+ import { createGlobalStateWithDecoupledFuncs } from 'react-native-global-state-hooks';
131
+
132
+ export const [useCount, getCount, $actions] =
133
+ createGlobalStateWithDecoupledFuncs(0, {
134
+ actions: {
135
+ log: (currentValue: string) => {
136
+ return ({ getState }: StoreTools<number>): void => {
137
+ console.log(`Current Value: ${getState()}`);
138
+ };
139
+ },
140
+
141
+ increase(value: number = 1) {
142
+ return ({ getState, setState }: StoreTools<number>) => {
143
+ setState((count) => count + value);
144
+
145
+ $actions.log(message);
146
+ };
147
+ },
148
+ },
149
+ });
150
+ ```
84
151
 
85
- ...
152
+ In the example the hook will work the same and you'll have access to the correct typing.
86
153
 
87
- # Decoupled hook
154
+ # Extending Global Hooks
88
155
 
89
- If you want to access the global state outside a component or outside a hook, or without subscribing the component to the state changes...
156
+ Creating a global hook that connects to an asyncStorage is made incredibly easy with the **createCustomGlobalState** function.
90
157
 
91
- This is especially useful when you want to create components that have edition access to a certain store, but they actually don't need to be reactive to the state changes, like a search component that just need to get the current state every time that is going to search the data; but actually don't need to be subscribed to the changes over the collection he is going to be filtering.
158
+ This function returns a new global state builder wrapped with the desired custom implementation, allowing you to get creative! Le'ts see and example:
92
159
 
93
160
  ```ts
94
- import { GlobalStore } from 'react-native-global-state-hooks';
161
+ import { formatFromStore, formatToStore, createCustomGlobalState } = 'react-native-global-state-hooks'
95
162
 
96
- const countStore = new GlobalStore(0);
163
+ // Optional configuration available for the consumers of the builder
164
+ type HookConfig = {
165
+ asyncStorageKey?: string;
166
+ };
97
167
 
98
- // remember this should be used as the **useState** hook.
99
- export const useCountGlobal = countStore.getHook();
168
+ // This is the base metadata that all the stores created from the builder will have.
169
+ type BaseMetadata = {
170
+ isAsyncStorageReady?: boolean;
171
+ };
100
172
 
101
- // this functions are not hooks, and they can be used in whatever place into your code, ClassComponents, OtherHooks, Services etc.
102
- export const [getCount, sendCount] = countStore.getHookDecoupled();
103
- ```
173
+ export const createGlobalState = createCustomGlobalState<
174
+ BaseMetadata,
175
+ HookConfig
176
+ >({
177
+ /**
178
+ * This function executes immediately after the global state is created, before the invocations of the hook
179
+ */
180
+ onInitialize: async ({ setState, setMetadata }, config) => {
181
+ setMetadata((metadata) => ({
182
+ ...(metadata ?? {}),
183
+ isAsyncStorageReady: null,
184
+ }));
185
+
186
+ const asyncStorageKey = config.asyncStorageKey;
187
+ if (!asyncStorageKey) return;
188
+
189
+ const storedItem = (await asyncStorage.getItem(asyncStorageKey)) as string;
190
+
191
+ // update the metadata, remember, metadata is not reactive
192
+ setMetadata((metadata) => ({
193
+ ...metadata,
194
+ isAsyncStorageReady: true,
195
+ }));
104
196
 
105
- Let's see a trivial example:
197
+ if (storedItem === null) {
198
+ return setState((state) => state, { forceUpdate: true });
199
+ }
106
200
 
107
- ...
201
+ const parsed = formatFromStore(storedItem, {
202
+ jsonParse: true,
203
+ });
108
204
 
109
- ```JSX
110
- import { useCountGlobal, sendCount } from './useCountGlobal'
205
+ setState(parsed, { forceUpdate: true });
206
+ },
111
207
 
112
- const CountDisplayerComponent: React.FC = () => {
113
- const [count] = useCountGlobal();
208
+ onChange: ({ getState }, config) => {
209
+ if (!config.asyncStorageKey) return;
114
210
 
115
- return (<Text>{count}<Text/>);
116
- }
211
+ const state = getState();
117
212
 
118
- // here we have a separate component that is gonna handle the state of the previous component we created,
119
- // this new component is not gonna be affected by the changes applied on <CountDisplayerComponent/>
120
- // Stage2 does not need to be updated once the global count changes
121
- const CountManagerComponent: React.FC = () => {
122
- const increaseClick = useCallback(() => sendCount(count => count + 1), []);
123
- const decreaseClick = useCallback(() => sendCount(count => count - 1), []);
124
-
125
- return (<>
126
- <Button onPress={increaseClick} title={'increase'} />
127
- <Button onPress={decreaseClick} title={'decrease'}/>
128
- </>);
129
- }
213
+ const formattedObject = formatToStore(state, {
214
+ stringify: true,
215
+ });
216
+
217
+ asyncStorage.setItem(config.asyncStorageKey, formattedObject);
218
+ },
219
+ });
130
220
  ```
131
221
 
132
- ...
222
+ 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.
133
223
 
134
- ...
224
+ It is worth mentioning that the **onInitialize** function will be executed only once per global state.
135
225
 
136
- # Extending the global storage
226
+ 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).
137
227
 
138
- Implementing extra functionality to extend the capabilities of the GlobalStorage couldn't be easier!!!
228
+ Let's see how to create a global state using our new builder:
229
+
230
+ ```ts
231
+ const useTodos = createGlobalState(new Map<string, number>(), {
232
+ config: {
233
+ asyncStorageKey: 'todos',
234
+ },
235
+ });
236
+ ```
139
237
 
140
- Here is an example of how you could create your custom store that for example stores the state into a async-storage persistente...
238
+ That's correct! If you add an **asyncStorageKey** to the state configuration, the state will be synchronized with the **asyncStorage**
141
239
 
142
- You could just use this code right as it is by just adding also into your project **@react-native-async-storage** or whatever another async storage library.
240
+ Let's see how to use this async storage hook into our components:
143
241
 
144
242
  ```ts
145
- import asyncStorage from '@react-native-async-storage/async-storage';
146
- import { formatFromStore, formatToStore } from 'json-storage-formatter';
243
+ const [todos, setTodos, metadata] = useTodos();
147
244
 
148
- import {
149
- GlobalStore as GlobalStoreBase,
150
- ActionCollectionConfig,
151
- StateChangesParam,
152
- StateConfigCallbackParam,
153
- StateSetter,
154
- } from 'react-native-global-state-hooks';
245
+ return (<>
246
+ {metadata.isAsyncStorageReady ? <TodoList todos={todos} /> : <Text>Loading...</Text>}
247
+ <>);
248
+ ```
249
+
250
+ The **metadata** is not reactive information and can only be modified from inside the global state lifecycle methods.
251
+
252
+ # Life cycle methods
155
253
 
254
+ There are some lifecycle methods available for use with global hooks, let's review them:
255
+
256
+ ```ts
257
+ /**
258
+ * @description callback function called when the store is initialized
259
+ * @returns {void} result - void
260
+ * */
261
+ onInit?: ({
262
+ /**
263
+ * Set the metadata
264
+ * @param {TMetadata} setter - The metadata or a function that will receive the metadata and return the new metadata
265
+ * */
266
+ setMetadata: MetadataSetter<TMetadata>;
267
+
268
+ /**
269
+ * Set the state
270
+ * @param {TState} setter - The state or a function that will receive the state and return the new state
271
+ * @param {{ forceUpdate?: boolean }} options - Options
272
+ * */
273
+ setState: StateSetter<TState>;
274
+
275
+ /**
276
+ * Get the state
277
+ * @returns {TState} result - The state
278
+ * */
279
+ getState: () => TState;
280
+
281
+ /**
282
+ * Get the metadata
283
+ * @returns {TMetadata} result - The metadata
284
+ * */
285
+ getMetadata: () => TMetadata;
286
+
287
+ /**
288
+ * Actions of the hook if configuration was provided
289
+ */
290
+ actions: TActions;
291
+ }: StateConfigCallbackParam<TState, TMetadata, TActions>) => void;
292
+
293
+ /**
294
+ * @description - callback function called every time the state is changed
295
+ */
296
+ onStateChanged?: (parameters: StateChangesParam<TState, TMetadata, TActions>) => void;
297
+
298
+ /**
299
+ * callback function called every time a component is subscribed to the store
300
+ */
301
+ onSubscribed?: (parameters: StateConfigCallbackParam<TState, TMetadata, TActions>) => void;
302
+
303
+ /**
304
+ * callback function called every time the state is about to change and it allows you to prevent the state change
305
+ */
306
+ computePreventStateChange?: (parameters: StateChangesParam<TState, TMetadata, TActions>) => boolean;
307
+ ```
308
+
309
+ You can pass this callbacks between on the second parameter of the builders like **createGlobalState**
310
+
311
+ ```ts
312
+ const useData = createGlobalState(
313
+ { value: 1 },
314
+ {
315
+ metadata: {
316
+ someExtraInformation: 'someExtraInformation',
317
+ },
318
+ // onSubscribed: (StateConfigCallbackParam) => {},
319
+ // onInit // etc
320
+ computePreventStateChange: ({ state, previousState }) => {
321
+ const prevent = isEqual(state, previousState);
322
+
323
+ return prevent;
324
+ },
325
+ }
326
+ );
327
+ ```
328
+
329
+ 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.
330
+
331
+ Let's see an example again with the **asyncStorage** custom global hook but with the abstract class.
332
+
333
+ ```ts
156
334
  export class GlobalStore<
157
335
  TState,
158
336
  TMetadata extends {
159
337
  asyncStorageKey?: string;
160
338
  isAsyncStorageReady?: boolean;
161
- },
339
+ } | null = null,
162
340
  TStateSetter extends
163
341
  | ActionCollectionConfig<TState, TMetadata>
164
342
  | StateSetter<TState> = StateSetter<TState>
@@ -166,9 +344,9 @@ export class GlobalStore<
166
344
  constructor(
167
345
  state: TState,
168
346
  config: GlobalStoreConfig<TState, TMetadata, TStateSetter> = {},
169
- setterConfig: TStateSetter | null = null
347
+ actionsConfig: TStateSetter | null = null
170
348
  ) {
171
- super(state, config, setterConfig);
349
+ super(state, config, actionsConfig);
172
350
 
173
351
  this.initialize();
174
352
  }
@@ -179,8 +357,13 @@ export class GlobalStore<
179
357
  getMetadata,
180
358
  getState,
181
359
  }: StateConfigCallbackParam<TState, TMetadata, TStateSetter>) => {
360
+ setMetadata({
361
+ ...(metadata ?? {}),
362
+ isAsyncStorageReady: null,
363
+ });
364
+
182
365
  const metadata = getMetadata();
183
- const { asyncStorageKey } = metadata;
366
+ const asyncStorageKey = metadata?.asyncStorageKey;
184
367
 
185
368
  if (!asyncStorageKey) return;
186
369
 
@@ -208,7 +391,7 @@ export class GlobalStore<
208
391
  getMetadata,
209
392
  getState,
210
393
  }: StateChangesParam<TState, TMetadata, NonNullable<TStateSetter>>) => {
211
- const { asyncStorageKey } = getMetadata();
394
+ const asyncStorageKey = getMetadata()?.asyncStorageKey;
212
395
 
213
396
  if (!asyncStorageKey) return;
214
397
 
@@ -223,566 +406,20 @@ export class GlobalStore<
223
406
  }
224
407
  ```
225
408
 
226
- The methods **formatToStore** and **formatFromStore** are part of another library of my [json-storage-formatter](https://www.npmjs.com/package/json-storage-formatter)...
227
-
228
- this super small library will help you to transform objects into json string and get them back without losing any of the original data types... no more problems when **stringify** Dates, Maps, Sets, etc...
229
-
230
- ## How to use the **GlobalStoreAsync**
231
-
232
- It will work exactly the same as the **GlobalStore**, the main difference is that by default the metadata object will include the {**isAsyncStorageReady** } which will allow you to know if the async data was already retrieved.
409
+ Then, from an instance of the global store, you will be able to access the hooks.
233
410
 
234
411
  ```ts
235
- const [count, setCount, { isAsyncStorageReady }] = useCountGlobal();
236
- ```
237
-
238
- ...
239
-
240
- Originally the library was implementing persistent storage by using the package **@react-native-async-storage**, but not all people want to use it or need to use it... so it has been removed. Feel free to use the above example to get back that functionality if you were using the previous versions of the package.
241
-
242
- You could find this example on the [**GitHub** of the project](https://github.com/johnny-quesada-developer/json-storage-formatter), between the unit test sources when the case scenario was tested [GlobalStoreAsyc.ts](https://github.com/johnny-quesada-developer/react-native-global-state-hooks/blob/master/%40tests/__test__/GlobalStoreAsyc.ts)
243
-
244
- ...
245
-
246
- ...
247
-
248
- # Restricting the manipulation of the global **state**
249
-
250
- ## Who hate reducers?
251
-
252
- It's super common to have the wish or the necessity of restricting the manipulation of the **state** through a specific set of actions or manipulations...**Dispatches**? **Actions**? Let's make it simple BY adding a custom **API** to the configuration of our **GlobalStore**
253
- ...
254
-
255
- ```ts
256
- const initialValue = 0;
257
-
258
- const config = {
259
- // this is not reactive information that you could also store in the async storage
260
- // upating the metadata will not trigger the onStateChanged method or any update on the components
261
- metadata: null,
262
-
263
- // The lifecycle callbacks are: onInit, onStateChanged, onSubscribed and computePreventStateChange
264
- };
265
-
266
- const countStore = new GlobalStore(
267
- initialValue,
268
- config,
269
- {
270
- log: (message: string) => (): void => {
271
- console.log(message);
272
- },
273
-
274
- increase(message: string) {
275
- return (storeTools: StoreTools<number>) => {
276
- this.log(message);
277
-
278
- return storeTools.getState();
279
- };
280
- },
281
-
282
- decrease(message: string) {
283
- return (storeTools: StoreTools<number>) => {
284
- this.log(message);
285
-
286
- return storeTools.getState();
287
- };
288
- },
289
- } as const // the -as const- is necessary to avoid typescript errors
290
- );
291
-
292
- // the way to get the hook is the same as for simple setters
293
- const useCountStore = countStore.getHook();
294
-
295
- // now instead of a setState method, you'll get an actions object
296
- // that contains all the actions that you defined in the setterConfig
297
- const [count, countActions] = useCountStore();
298
-
299
- // count is the current state - 0 (number)
300
- // countActions is an object that contains all the actions that you defined in the setterConfig
301
- // countActions.increase(); // this will increase the count by 1, returns the new count (number)
302
- // countActions.decrease(); // this will decrease the count by 1, returns the new count (number)
303
- ```
304
-
305
- ...
306
-
307
- # Configuration callbacks
308
-
309
- ## config.onInit
310
-
311
- This method will be called once the store is created after the constructor,
312
-
313
- @examples
314
-
315
- ```ts
316
- import { GlobalStore } from 'react-native-global-state-hooks';
317
-
318
- const initialValue = 0;
319
-
320
- const store = new GlobalStore(0, {
321
- onInit: async ({ setMetadata, setState }) => {
322
- const data = await someApiCall();
323
-
324
- setState(data);
325
- setMetadata({ isDataUpdated: true });
326
- },
327
- });
328
- ```
329
-
330
- ...
331
-
332
- ## config.onStateChanged
333
-
334
- This method will be called every time the state is changed
335
-
336
- @examples
337
-
338
- ```ts
339
- import { GlobalStore } from 'react-native-global-state-hooks';
340
-
341
- const store = new GlobalStore(0, {
342
- onStateChanged: ({ getState }) => {
343
- const state = getState();
344
-
345
- console.log(state);
346
- },
347
- });
348
- ```
349
-
350
- ...
351
-
352
- ## config.onSubscribed
353
-
354
- This method will be called every time a component is subscribed to the store
355
-
356
- @examples
357
-
358
- ```ts
359
- import { GlobalStore } from 'react-native-global-state-hooks';
360
-
361
- const store = new GlobalStore(0, {
362
- onSubscribed: ({ getState }) => {
363
- console.log('A component was subscribed to the store');
364
- },
365
- });
366
- ```
367
-
368
- ...
369
-
370
- ## config.computePreventStateChange
371
-
372
- This method will be called every time the state is going to be changed, if it returns true the state won't be changed
373
-
374
- @examples
375
-
376
- ```ts
377
- import { GlobalStore } from 'react-native-global-state-hooks';
378
-
379
- const store = new GlobalStore(0, {
380
- computePreventStateChange: ({ getState }) => {
381
- const state = getState();
382
- const shouldPrevent = state < 0;
383
-
384
- if (shouldPrevent) return true;
385
-
386
- return false;
387
- },
388
- });
389
- ```
390
-
391
- ...
392
-
393
- ...
394
-
395
- ...
396
-
397
- ...
398
-
399
- ...
400
-
401
- # Examples and Comparison:
402
-
403
- ## 1. Lets try to share some state between components
404
-
405
- ### **With the GlobalStore approach, it will look like this:**
406
-
407
- ```tsx
408
- type TUser = {
409
- name: string;
410
- email: string;
411
- };
412
-
413
- const useUserStore = new GlobalStore<TUser>({
414
- name: null,
415
- email: null,
416
- }).getHook();
417
-
418
- const Component = () => {
419
- const [currentUser] = useUserStore();
420
-
421
- return <Text>{currentUser.name}</Text>;
422
- };
423
- ```
424
-
425
- ## Simple, right?
426
-
427
- ### Let's now see how this same thing would look like by using context:
428
-
429
- ```tsx
430
- type TUser = {
431
- name: string;
432
- email: string;
433
- };
434
-
435
- const UserContext = createContext<{
436
- currentUser: TUser;
437
- }>({
438
- currentUser: null,
439
- });
440
-
441
- const UserProvider: React.FC<PropsWithChildren> = ({ children }) => {
442
- const [currentUser, setCurrentUser] = useState<TUser>(null);
443
-
444
- // ...get current user information
445
-
446
- return (
447
- <UserContext.Provider value={{ currentUser }}>
448
- {children}
449
- </UserContext.Provider>
450
- );
451
- };
452
-
453
- const Component = () => {
454
- const { currentUser } = useContext(UserContext);
455
-
456
- return <Text>{currentUser.name}</Text>;
457
- };
458
-
459
- const App = () => {
460
- return (
461
- <UserProvider>
462
- <Component />
463
- </UserProvider>
464
- );
465
- };
466
- ```
467
-
468
- ### We already are able to notice a couple of extra lines right?
469
-
470
- Let's now add another simple store to the equation
471
-
472
- ### **With the GlobalStore approach, it will look like this:**
473
-
474
- ```tsx
475
- type TUser = {
476
- name: string;
477
- email: string;
478
- };
479
-
480
- const useUserStore = new GlobalStore<TUser>({
481
- name: null,
482
- email: null,
483
- }).getHook();
484
-
485
- // we create the store
486
- const useCountStore = new GlobalStore(0).getHook();
487
-
488
- const Component = () => {
489
- const [currentUser] = useUserStore();
490
-
491
- // from the component we consume the new store
492
- const [count, setCount] = useCountStore();
493
-
494
- return <Text>{currentUser.name}</Text>;
495
- };
496
- ```
497
-
498
- With context, we'll have again to create all the boilerplate, and wrap the component into the new provider...
499
-
500
- ### **Lets see that**
501
-
502
- ```tsx
503
- type TUser = {
504
- name: string;
505
- email: string;
506
- };
507
-
508
- const UserContext = createContext<{
509
- currentUser: TUser;
510
- }>({
511
- currentUser: null,
512
- });
513
-
514
- // let's create the context
515
- const CountContext = createContext({
516
- count: 0,
517
- setCount: (() => {
518
- throw new Error('not implemented');
519
- }) as Dispatch<SetStateAction<number>>,
520
- });
521
-
522
- const UserProvider: React.FC<PropsWithChildren> = ({ children }) => {
523
- const [currentUser, setCurrentUser] = useState<TUser>(null);
524
-
525
- // ...
526
-
527
- return (
528
- <UserContext.Provider value={{ currentUser }}>
529
- {children}
530
- </UserContext.Provider>
531
- );
532
- };
533
-
534
- // we also need another provider
535
- const CountProvider: React.FC<PropsWithChildren> = ({ children }) => {
536
- const [count, setCount] = useState(0);
537
-
538
- return (
539
- <CountContext.Provider value={{ count, setCount }}>
540
- {children}
541
- </CountContext.Provider>
542
- );
543
- };
544
-
545
- // we need to wrap the component into the new provider (this is for each future context)
546
- const App = () => {
547
- return (
548
- <UserProvider>
549
- <CountProvider>
550
- <Component />
551
- </CountProvider>
552
- </UserProvider>
553
- );
554
- };
555
-
556
- const Component = () => {
557
- const { currentUser } = useContext(UserContext);
558
-
559
- // finally we are able to get access to the new context...
560
- const { count, setCount } = useContext(CountContext);
561
-
562
- return <Text>{currentUser.name}</Text>;
563
- };
564
- ```
565
-
566
- In this example, we are able to see how every time along with creating a good amount of repetitive code, we also have to wrap the necessary components into the Provider... Also, notice how every time we need to modify the **App** component, even when the App component is not gonna use the new state.
567
-
568
- ### Let's make this a little more complex, now I want to implement custom methods for manipulating the count state, I also want to have the ability to modify the count state **without** having to be subscribed to the changes of the state... have you ever done that?
569
-
570
- This is a common scenery, and guess what? in the **context** examples, we'll have to create another context, another provider, wrap and everything again...
571
-
572
- ## Let's see this time first the **context** approach
573
-
574
- ```tsx
575
- type TUser = {
576
- name: string;
577
- email: string;
578
- };
579
-
580
- const UserContext = createContext<{
581
- currentUser: TUser;
582
- }>({
583
- currentUser: null,
584
- });
585
-
586
- // let's remove the setter from this context
587
- const CountContext = createContext({
588
- count: 0,
589
- });
590
-
591
- // lets create another context to share the actions
592
- const CountContextSetter = createContext({
593
- increase: (): void => {
594
- throw new Error('increase is not implemented');
595
- },
596
- decrease: (): void => {
597
- throw new Error('decrease is not implemented');
598
- },
599
- });
600
-
601
- const UserProvider: React.FC<PropsWithChildren> = ({ children }) => {
602
- const [currentUser, setCurrentUser] = useState<TUser>(null);
603
-
604
- // ...
605
-
606
- return (
607
- <UserContext.Provider value={{ currentUser }}>
608
- {children}
609
- </UserContext.Provider>
610
- );
611
- };
612
-
613
- // To don't overcomplicate the example let's just add but providers into this component, that will be enough
614
- const CountProvider: React.FC<PropsWithChildren> = ({ children }) => {
615
- const [count, setCount] = useState(0);
616
-
617
- const increase = () => setCount(count + 1);
618
- const decrease = () => setCount(count - 1);
619
-
620
- return (
621
- //one context is gonna share the edition of the state
622
- <CountContext.Provider value={{ count }}>
623
- {/* this second component will share the mutations of the state */}
624
- <CountContextSetter.Provider value={{ increase, decrease }}>
625
- {children}
626
- </CountContextSetter.Provider>
627
- </CountContext.Provider>
628
- );
629
- };
630
-
631
- // Since we used the same provider we don't need to modify the **App** component, but we do are **Wrapping** everything into one more **Provider**
632
- const App = () => {
633
- return (
634
- <UserProvider>
635
- <CountProvider>
636
- {/* lets create two componets instead of one */}
637
- <ComponentSetter />
638
- <Component />
639
- </CountProvider>
640
- </UserProvider>
641
- );
642
- };
643
-
644
- const ComponentSetter = () => {
645
- const { increase, decrease } = useContext(CountContextSetter);
646
-
647
- return (
648
- <View>
649
- <Button title='Increase' onPress={increase} />
650
- <Button title='Decrease' onPress={decrease} />
651
- </View>
652
- );
653
- };
654
-
655
- const Component = () => {
656
- const { currentUser } = useContext(UserContext);
657
-
658
- // finally we are able to get access to the new context...
659
- const { count } = useContext(CountContext);
660
-
661
- return (
662
- <View>
663
- <Text>{currentUser.name}</Text>
664
- <Text>{count}</Text>
665
- </View>
666
- );
667
- };
668
- ```
669
-
670
- Wow, a lot!!! just to be able to separate the mutations... and have mutations!!
671
-
672
- ### it would be easier with the GlobalStore? Let's see.
673
-
674
- ```tsx
675
- type TUser = {
676
- name: string;
677
- email: string;
678
- };
679
-
680
- const useUser = new GlobalStore<TUser>({
681
- name: null,
682
- email: null,
683
- }).getHook();
684
-
685
- // let's modify the store to add custom actions, the second parameter is configuration let's just pass null for now
686
- const countStore = new GlobalStore(0, null, {
687
- increase() {
688
- return ({ setState }: StoreTools<number>) => {
689
- setState((state) => state + 1);
690
- };
691
- },
692
-
693
- decrease() {
694
- return ({ setState }: StoreTools<number>) => {
695
- setState((state) => state - 1);
696
- };
697
- },
698
- } as const);
699
-
700
- const useCount = countStore.getHook();
701
-
702
- // this actions don't use hooks, but are connected to the store and all the subscribers will be notified
703
- const [, countActions] = countStore.getHookDecoupled();
704
-
705
- // this component is not subscribed to the store, so it will not be notified when the state changes
706
- const ComponentSetter = () => {
707
- return (
708
- <View>
709
- <Button title='Increase' onPress={countActions.increase} />
710
- <Button title='Decrease' onPress={countActions.decrease} />
711
- </View>
712
- );
713
- };
714
-
715
- // this component is subscribed to the store, so it will be notified when the state changes
716
- const Component = () => {
717
- const [user] = useUser();
718
- const [count, actions] = useCount();
719
-
720
- return (
721
- <View>
722
- <Text>{count}</Text>
723
- </View>
724
- );
725
- };
726
- ```
727
-
728
- ### So let's analyze what happened
729
-
730
- To restrict the state manipulations with the custom actions, we just need to add a third parameter to the store.
731
-
732
- ```ts
733
- const countStore = new GlobalStore(0, null, {
734
- log: (action: string) => () => console.log(action),
735
-
736
- // every action is a function that returns a function that receives the store tools
737
- increase() {
738
- return ({ setState, getState }: StoreTools<number>): number => {
739
- setState((state) => state + 1);
740
-
741
- // actions are able to communicate between them
742
- this.log('increase');
743
-
744
- return getState();
745
- };
746
- },
747
- } as const);
748
-
749
- // the const is necessary to avoid typescript errors
750
- ```
751
-
752
- All the library is strongly typed, we use generics to return the correct data type in each action.
753
-
754
- ```ts
755
- const [, actions] = countStore.getHookDecoupled();
756
-
757
- // for example the type of actions.increase will be: () => number
758
- // just in case, even the parameters of the actions are gonna be exposed through TS
759
- ```
760
-
761
- ## getHookDecoupled
762
-
763
- ### **getHookDecoupled** returns a tuple with the state and the actions,
764
-
765
- This is so useful when you want to use the actions without having to be subscribed to changes of the state.
766
- There is also a third element in the tuple which is a function for getting the metadata of the store
767
-
768
- ### the metadata of the store is not reactive information which could be shared through the store
769
-
770
- ## Adding metadata to the store
771
-
772
- ```tsx
773
- const [, , getMetadata] = new GlobalStore(0, {
412
+ const storage = new GlobalStore(0, {
774
413
  metadata: {
775
- isStoredSyncronized: false,
414
+ asyncStorageKey: 'counter',
415
+ isAsyncStorageReady: false,
776
416
  },
777
- }).getHookDecoupled();
417
+ });
778
418
 
779
- console.log(getMetadata().isStoredSyncronized); // false
419
+ const [getState, _, getMetadata] = storage.getHookDecoupled();
420
+ const useState = storage.getHook();
780
421
  ```
781
422
 
782
- The setMetadata is part of the store tools, so it can be used in the actions, but againg the metadata is not reactive!! so it will not trigger a re-render on the subscribers
783
-
784
- ...
785
-
786
- ...
423
+ ### **Note**: The GlobalStore class is still available in the package in case you were already extending from it.
787
424
 
788
425
  # That's it for now!! hope you enjoy coding!!