react-global-state-hooks 1.1.1 → 2.0.0
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 +574 -499
- package/lib/bundle.js +1 -2
- package/lib/src/GlobalStore.d.ts +18 -0
- package/lib/src/GlobalStore.functionHooks.d.ts +18 -0
- package/lib/src/GlobalStore.types.d.ts +43 -0
- package/lib/src/GlobalStore.utils.d.ts +18 -0
- package/lib/src/GlobalStoreAbstract.d.ts +16 -0
- package/lib/src/index.d.ts +12 -0
- package/package.json +5 -3
- package/lib/GlobalStore.d.ts +0 -66
- package/lib/GlobalStore.types.d.ts +0 -24
- package/lib/bundle.js.LICENSE.txt +0 -1
- package/lib/index.d.ts +0 -2
package/README.md
CHANGED
|
@@ -1,694 +1,769 @@
|
|
|
1
1
|
# react-global-state-hooks
|
|
2
2
|
|
|
3
|
-
This is a package to easily handling global
|
|
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 an introduction video [here!](https://www.youtube.com/watch?v=WfoMhO1zZ04)
|
|
3
|
+
This is a package to easily handling **global state hooks** across your **react components**
|
|
6
4
|
|
|
7
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/)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
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).
|
|
12
8
|
|
|
13
|
-
|
|
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**
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { GlobalStore } from 'react-global-state-hooks';
|
|
11
|
+
You can also see an introduction video [here!](https://www.youtube.com/watch?v=WfoMhO1zZ04&t=8s)
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
const countStore = new GlobalStore(0);
|
|
13
|
+
# Creating a global state
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
export const useCountGlobal = countStore.getHook();
|
|
15
|
+
We are gonna create a global state hook **useCount** with one line of code.
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// That's it, that's a global store... Strongly typed, with a global-hook that we could reuse cross all our react-components.
|
|
17
|
+
```ts
|
|
18
|
+
import { createGlobalState } from 'react-global-state-hooks';
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
export const useCount = createGlobalState(0);
|
|
21
|
+
```
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
That's it! Welcome to global hooks. Now, you can use this state wherever you need it in your application.
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
console.log(getCount()); // 0;
|
|
25
|
+
Let's see how to use it inside a simple **component**
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
```ts
|
|
28
|
+
const [count, setCount] = useCount();
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
return <button onclick={() => setCount((count) => count + 1)}>{count}</button>;
|
|
43
31
|
```
|
|
44
32
|
|
|
45
|
-
|
|
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.
|
|
34
|
+
|
|
35
|
+
# Selectors
|
|
46
36
|
|
|
47
|
-
|
|
37
|
+
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**
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
```ts
|
|
40
|
+
import { createGlobalState } from 'react-global-state-hooks';
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
export const useContacts = createGlobalState({
|
|
43
|
+
isLoading: true,
|
|
44
|
+
filter: '',
|
|
45
|
+
items: [] as Contact[],
|
|
46
|
+
});
|
|
47
|
+
```
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
import { useCountGlobal } from './useCountGlobal'
|
|
49
|
+
Now, let's say we want to have a filter bar for the contacts that will only have access to the filter.
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
const [count, setter] = useCountGlobal();
|
|
58
|
-
const onClickAddOne = () => setter(count + 1);
|
|
51
|
+
**FilterBar.tsx**
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
}
|
|
53
|
+
```ts
|
|
54
|
+
const [{ filter }, setState] = useContacts(({ filter }) => ({ filter }));
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<input
|
|
58
|
+
onChange={(event) =>
|
|
59
|
+
setState((state) => ({ ...state, filter: event.target.value }))
|
|
60
|
+
}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
```
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
const [count, setter] = useCountGlobal();
|
|
65
|
+
There you have it again, super simple! By adding a **selector** function, you are able to create a derivative hook that will only trigger when the result of the **selector** changes.
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
const onClickAddTwo = useCallback(() => setter(state => state + 2), [])
|
|
67
|
+
By the way, in the example, the **selector** returning a new object is not a problem at all. This is because, by default, there is a shallow comparison between the previous and current versions of the state, so the render won't trigger if it's not necessary.
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
}
|
|
69
|
+
## What if you want to reuse the selector?
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
```
|
|
71
|
+
It will be super common to have the necessity of reusing a specific **selector**, and it can be a little annoying to have to do the same thing again and again. Right?
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
No problem, you can create a reusable **derivative-state** and use it across your components. Let's create one for our filter.
|
|
76
74
|
|
|
77
75
|
```ts
|
|
78
|
-
const
|
|
76
|
+
const useFilter = createDerivate(useContacts, ({ filter }) => ({ filter }));
|
|
79
77
|
```
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
...
|
|
84
|
-
|
|
85
|
-
# Persisting state into localhost
|
|
79
|
+
Well, that's it! Now you can simply call **useFilter** inside your component, and everything will continue to work the same.
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
**FilterBar.tsx**
|
|
88
82
|
|
|
89
83
|
```ts
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
const [{ filter }, setState] = useFilter();
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<input
|
|
88
|
+
onChange={(event) =>
|
|
89
|
+
setState((state) => ({ ...state, filter: event.target.value }))
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
99
93
|
```
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
...
|
|
95
|
+
Notice that the **state** changes, but the **setter** does not. This is because this is a **DERIVATE state**, and it cannot be directly changed. It will always be derived from the main hook.
|
|
104
96
|
|
|
105
|
-
#
|
|
97
|
+
# State actions
|
|
106
98
|
|
|
107
|
-
|
|
99
|
+
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 **useContacts**.
|
|
108
100
|
|
|
109
|
-
|
|
101
|
+
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.
|
|
110
102
|
|
|
111
103
|
```ts
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
const countStore = new GlobalStore(0);
|
|
104
|
+
import { createGlobalState } from 'react-global-state-hooks';
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
const initialState = {
|
|
107
|
+
isLoading: true,
|
|
108
|
+
filter: '',
|
|
109
|
+
items: [] as Contact[],
|
|
110
|
+
};
|
|
118
111
|
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
type State = typeof initialState;
|
|
113
|
+
|
|
114
|
+
export const useContacts = createGlobalState(initialState, {
|
|
115
|
+
// this are the actions available for this state
|
|
116
|
+
actions: {
|
|
117
|
+
setFilter(filter: string) {
|
|
118
|
+
return ({ setState }: StoreTools<State>) => {
|
|
119
|
+
setState((state) => ({
|
|
120
|
+
...state,
|
|
121
|
+
filter,
|
|
122
|
+
}));
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
} as const,
|
|
126
|
+
onInit: async ({ setState }: StoreTools<State>) => {
|
|
127
|
+
// fetch contacts
|
|
128
|
+
},
|
|
129
|
+
});
|
|
121
130
|
```
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
...
|
|
126
|
-
|
|
127
|
-
```JSX
|
|
128
|
-
import { useCountGlobal, sendCount } from './useCountGlobal'
|
|
132
|
+
That's it! In this updated version, the **useContacts** 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.
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
const [count] = useCountGlobal();
|
|
134
|
+
Let's see how that will look now into our **FilterBar.tsx**
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
}
|
|
136
|
+
```tsx
|
|
137
|
+
const [{ filter }, { setFilter }] = useFilter();
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
// this new component is not gonna be affected by the changes applied on <CountDisplayerComponent/>
|
|
138
|
-
// Stage2 does not need to be updated once the global count changes
|
|
139
|
-
const CountManagerComponent: React.FC = () => {
|
|
140
|
-
const increaseClick = useCallback(() => sendCount(count => count + 1), []);
|
|
141
|
-
const decreaseClick = useCallback(() => sendCount(count => count - 1), []);
|
|
142
|
-
|
|
143
|
-
return (<>
|
|
144
|
-
<button onClick={increaseClick} >increase</button>
|
|
145
|
-
<button onClick={decreaseClick} >decrease</button>
|
|
146
|
-
</>);
|
|
147
|
-
}
|
|
139
|
+
return <input onChange={(event) => setFilter(event.target.value)} />;
|
|
148
140
|
```
|
|
149
141
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
...
|
|
153
|
-
|
|
154
|
-
# Restricting the manipulation of the global **state**
|
|
155
|
-
|
|
156
|
-
## Who hate reducers?
|
|
142
|
+
Yeah, that's it! All the **derived states** and **emitters** (we will talk about this later) will inherit the new actions interface.
|
|
157
143
|
|
|
158
|
-
|
|
159
|
-
...
|
|
144
|
+
You can even **derive** from another **derived state**! Let's explore a few silly examples:
|
|
160
145
|
|
|
161
146
|
```ts
|
|
162
|
-
const
|
|
147
|
+
const useFilter = createDerivate(useContacts, ({ filter }) => ({ filter }));
|
|
163
148
|
|
|
164
|
-
const
|
|
165
|
-
// this is not reactive information that you could also store in the async storage
|
|
166
|
-
// upating the metadata will not trigger the onStateChanged method or any update on the components
|
|
167
|
-
metadata: null,
|
|
149
|
+
const useFilterString = createDerivate(useFilter, { filter } => filter);
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
};
|
|
151
|
+
const useContacts = createDerivate(useContacts, ({ items }) => items);
|
|
171
152
|
|
|
172
|
-
const
|
|
173
|
-
initialValue,
|
|
174
|
-
config,
|
|
175
|
-
{
|
|
176
|
-
log: (message: string) => (): void => {
|
|
177
|
-
console.log(message);
|
|
178
|
-
},
|
|
153
|
+
const useContactsLength = createDerivate(useContacts, (items) => items.length);
|
|
179
154
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.log(message);
|
|
155
|
+
const useIsContactsEmpty = createDerivate(useContactsLength, (length) => !length);
|
|
156
|
+
```
|
|
183
157
|
|
|
184
|
-
|
|
185
|
-
};
|
|
186
|
-
},
|
|
158
|
+
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.
|
|
187
159
|
|
|
188
|
-
|
|
189
|
-
return (storeTools: StoreTools<number>) => {
|
|
190
|
-
this.log(message);
|
|
160
|
+
# Decoupled state access
|
|
191
161
|
|
|
192
|
-
|
|
193
|
-
};
|
|
194
|
-
},
|
|
195
|
-
} as const // the -as const- is necessary to avoid typescript errors
|
|
196
|
-
);
|
|
162
|
+
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 the **createGlobalStateWithDecoupledFuncs**.
|
|
197
163
|
|
|
198
|
-
|
|
199
|
-
const useCountStore = countStore.getHook();
|
|
164
|
+
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.
|
|
200
165
|
|
|
201
|
-
|
|
202
|
-
// that contains all the actions that you defined in the setterConfig
|
|
203
|
-
const [count, countActions] = useCountStore();
|
|
166
|
+
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:
|
|
204
167
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
168
|
+
```ts
|
|
169
|
+
import { createGlobalStateWithDecoupledFuncs } from 'react-global-state-hooks';
|
|
170
|
+
|
|
171
|
+
export const [useContacts, contactsGetter, contactsSetter] =
|
|
172
|
+
createGlobalStateWithDecoupledFuncs({
|
|
173
|
+
isLoading: true,
|
|
174
|
+
filter: '',
|
|
175
|
+
items: [] as Contact[],
|
|
176
|
+
});
|
|
209
177
|
```
|
|
210
178
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Configuration callbacks
|
|
214
|
-
|
|
215
|
-
## config.onInit
|
|
216
|
-
|
|
217
|
-
This method will be called once the store is created after the constructor,
|
|
179
|
+
That's great! With the addition of the **contactsGetter** and **contactsSetter** methods, you now have the ability to access and modify the state without the need for subscription to the hook.
|
|
218
180
|
|
|
219
|
-
|
|
181
|
+
While **useContacts** will allow your components to subscribe to the custom hook, using the **contactsGetter** method you will be able retrieve the current value of the state. This allows you to access the state whenever necessary, without being reactive to its changes. Let' see how:
|
|
220
182
|
|
|
221
183
|
```ts
|
|
222
|
-
|
|
184
|
+
// To synchronously get the value of the state
|
|
185
|
+
const value = contactsGetter();
|
|
223
186
|
|
|
224
|
-
|
|
187
|
+
// the type of value will be { isLoading: boolean; filter: string; items: Contact[] }
|
|
188
|
+
```
|
|
225
189
|
|
|
226
|
-
|
|
227
|
-
onInit: async ({ setMetadata, setState }) => {
|
|
228
|
-
const data = await someApiCall();
|
|
190
|
+
Additionally, to subscribe to state changes, you can pass a callback function as a parameter to the **getter**. This approach enables you to create a subscription group, allowing you to subscribe to either the entire state or a specific portion of it. When a callback function is provided to the **getter**, it will return a cleanup function instead of the state. This cleanup function can be used to unsubscribe or clean up the subscription when it is no longer needed.
|
|
229
191
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
192
|
+
```ts
|
|
193
|
+
/**
|
|
194
|
+
* This not only allows you to retrieve the current value of the state...
|
|
195
|
+
* but also enables you to subscribe to any changes in the state or a portion of it
|
|
196
|
+
*/
|
|
197
|
+
const removeSubscriptionGroup = contactsGetter<Subscribe>((subscribe) => {
|
|
198
|
+
subscribe((state) => {
|
|
199
|
+
console.log('state changed: ', state);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
subscribe(
|
|
203
|
+
(state) => state.isLoading,
|
|
204
|
+
(isLoading) => {
|
|
205
|
+
console.log('is loading changed', isLoading);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
233
208
|
});
|
|
234
209
|
```
|
|
235
210
|
|
|
236
|
-
|
|
211
|
+
That's great, isn't it? everything stays synchronized with the original state!!
|
|
237
212
|
|
|
238
|
-
|
|
213
|
+
# Emitters
|
|
239
214
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
@examples
|
|
215
|
+
So, we have seen that we can subscribe a callback to state changes, create **derivative states** from our global hooks, **and derive hooks from those derivative states**. Guess what? We can also create derivative **emitters** and subscribe callbacks to specific portions of the state. Let's review it:
|
|
243
216
|
|
|
244
217
|
```ts
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
console.log(state);
|
|
252
|
-
},
|
|
253
|
-
});
|
|
218
|
+
const subscribeToFilter = createDerivateEmitter(
|
|
219
|
+
contactsGetter,
|
|
220
|
+
({ filter }) => ({
|
|
221
|
+
filter,
|
|
222
|
+
})
|
|
223
|
+
);
|
|
254
224
|
```
|
|
255
225
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
## config.onSubscribed
|
|
259
|
-
|
|
260
|
-
This method will be called every time a component is subscribed to the store
|
|
226
|
+
Cool, it's basically the same, but instead of using the **hook** as a parameter, we just have to use the **getter** as a parameter, and that will make the magic.
|
|
261
227
|
|
|
262
|
-
|
|
228
|
+
Now we are able to add a callback that will be executed every time the state of the **filter** changes.
|
|
263
229
|
|
|
264
230
|
```ts
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const store = new GlobalStore(0, {
|
|
268
|
-
onSubscribed: ({ getState }) => {
|
|
269
|
-
console.log('A component was subscribed to the store');
|
|
270
|
-
},
|
|
231
|
+
const removeFilterSubscription = subscribeToFilter<Subscribe>(({ filter }) => {
|
|
232
|
+
console.log(`The filter value changed: ${filter}`);
|
|
271
233
|
});
|
|
272
234
|
```
|
|
273
235
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
## config.computePreventStateChange
|
|
277
|
-
|
|
278
|
-
This method will be called every time the state is going to be changed, if it returns true the state won't be changed
|
|
279
|
-
|
|
280
|
-
@examples
|
|
236
|
+
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.
|
|
281
237
|
|
|
282
238
|
```ts
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
computePreventStateChange: ({ getState }) => {
|
|
287
|
-
const state = getState();
|
|
288
|
-
const shouldPrevent = state < 0;
|
|
289
|
-
|
|
290
|
-
if (shouldPrevent) return true;
|
|
291
|
-
|
|
292
|
-
return false;
|
|
239
|
+
const removeFilterSubscription = subscribeToFilter<Subscribe>(
|
|
240
|
+
({ filter }) => {
|
|
241
|
+
console.log(`The filter value changed: ${filter}`);
|
|
293
242
|
},
|
|
294
|
-
|
|
243
|
+
{
|
|
244
|
+
skipFirst: true,
|
|
245
|
+
}
|
|
246
|
+
);
|
|
295
247
|
```
|
|
296
248
|
|
|
297
|
-
|
|
249
|
+
Also, of course, if you have an exceptional case where you want to derivate directly from the current **emitter**, you can add a **selector**. This allows you to fine-tune the emitted values based on your requirements
|
|
298
250
|
|
|
299
|
-
|
|
251
|
+
```ts
|
|
252
|
+
const removeFilterSubscription = subscribeToFilter<Subscribe>(
|
|
253
|
+
({ filter }) => filter,
|
|
254
|
+
/**
|
|
255
|
+
* Cause of the selector the filter now is an string
|
|
256
|
+
*/
|
|
257
|
+
(filter) => {
|
|
258
|
+
console.log(`The filter value changed: ${filter}`);
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
skipFirst: true,
|
|
262
|
+
/**
|
|
263
|
+
* You can also override the default shallow comparison...
|
|
264
|
+
* or disable it completely by setting the isEqual callback to null.
|
|
265
|
+
*/
|
|
266
|
+
isEqual: (a, b) => a === b,
|
|
267
|
+
// isEqual: null // this will avoid doing a shallow comparison
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
```
|
|
300
271
|
|
|
301
|
-
|
|
272
|
+
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:
|
|
302
273
|
|
|
303
|
-
|
|
274
|
+
```ts
|
|
275
|
+
const subscribeToItems = createDerivateEmitter(
|
|
276
|
+
contactsGetter,
|
|
277
|
+
({ items }) => items
|
|
278
|
+
);
|
|
304
279
|
|
|
305
|
-
|
|
280
|
+
const subscribeToItemsLength = createDerivateEmitter(
|
|
281
|
+
subscribeToItems,
|
|
282
|
+
(items) => items.length
|
|
283
|
+
);
|
|
284
|
+
```
|
|
306
285
|
|
|
307
|
-
|
|
286
|
+
The examples may seem a little silly, but they allow you to see the incredible things you can accomplish with these **derivative states** and **emitters**. They open up a world of possibilities!
|
|
308
287
|
|
|
309
|
-
|
|
288
|
+
# Combining getters
|
|
310
289
|
|
|
311
|
-
|
|
290
|
+
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 **getters**.
|
|
312
291
|
|
|
313
|
-
|
|
314
|
-
type TUser = {
|
|
315
|
-
name: string;
|
|
316
|
-
email: string;
|
|
317
|
-
};
|
|
292
|
+
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:
|
|
318
293
|
|
|
319
|
-
|
|
320
|
-
name: null,
|
|
321
|
-
email: null,
|
|
322
|
-
}).getHook();
|
|
294
|
+
Fist we are gonna create a couple of **global state**, is important to create them with the **createGlobalStateWithDecoupledFuncs** since we need the decoupled **getter**. (In case you are using an instance of **GlobalStore** or **GlobalStoreAbstract** you can just pick up the getters from the **getHookDecoupled** method)
|
|
323
295
|
|
|
324
|
-
|
|
325
|
-
|
|
296
|
+
```ts
|
|
297
|
+
const [useHook1, getter1, setter1] = createGlobalStateWithDecoupledFuncs({
|
|
298
|
+
propA: 1,
|
|
299
|
+
propB: 2,
|
|
300
|
+
});
|
|
326
301
|
|
|
327
|
-
|
|
328
|
-
|
|
302
|
+
const [, getter2] = createGlobalStateWithDecoupledFuncs({
|
|
303
|
+
propC: 3,
|
|
304
|
+
propD: 4,
|
|
305
|
+
});
|
|
329
306
|
```
|
|
330
307
|
|
|
331
|
-
|
|
308
|
+
Okay, cool, the first state as **propA, propB** while the second one has **propC, propD**, let's combine them:
|
|
332
309
|
|
|
333
|
-
|
|
310
|
+
```ts
|
|
311
|
+
const [useCombinedHook, getter, dispose] = combineAsyncGetters(
|
|
312
|
+
{
|
|
313
|
+
selector: ([state1, state2]) => ({
|
|
314
|
+
...state1,
|
|
315
|
+
...state2,
|
|
316
|
+
}),
|
|
317
|
+
},
|
|
318
|
+
getter1,
|
|
319
|
+
getter2
|
|
320
|
+
);
|
|
321
|
+
```
|
|
334
322
|
|
|
335
|
-
|
|
336
|
-
type TUser = {
|
|
337
|
-
name: string;
|
|
338
|
-
email: string;
|
|
339
|
-
};
|
|
323
|
+
Well, that's it! Now you have access to a **getter** that will return the combined value of the two states. From this new **getter**, you can retrieve the value or subscribe to its changes. Let'see:
|
|
340
324
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
325
|
+
```ts
|
|
326
|
+
const value = getter(); // { propA, propB, propC, propD }
|
|
327
|
+
|
|
328
|
+
// subscribe to the new emitter
|
|
329
|
+
const unsubscribeGroup = getter<Subscribe>((subscribe) => {
|
|
330
|
+
subscribe((state) => {
|
|
331
|
+
console.log(subscribe); // full state
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Please note that if you add a selector,
|
|
335
|
+
// the callback will only trigger if the result of the selector changes.
|
|
336
|
+
subscribe(
|
|
337
|
+
({ propA, propD }) => ({ propA, propD }),
|
|
338
|
+
(derivate) => {
|
|
339
|
+
console.log(derivate); // { propA, propD }
|
|
340
|
+
}
|
|
341
|
+
);
|
|
345
342
|
});
|
|
343
|
+
```
|
|
346
344
|
|
|
347
|
-
|
|
348
|
-
const [currentUser, setCurrentUser] = useState<TUser>(null);
|
|
345
|
+
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.
|
|
349
346
|
|
|
350
|
-
|
|
347
|
+
```ts
|
|
348
|
+
const [combinedState] = useCombinedHook();
|
|
349
|
+
```
|
|
351
350
|
|
|
352
|
-
|
|
353
|
-
<UserContext.Provider value={{ currentUser }}>
|
|
354
|
-
{children}
|
|
355
|
-
</UserContext.Provider>
|
|
356
|
-
);
|
|
357
|
-
};
|
|
351
|
+
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.
|
|
358
352
|
|
|
359
|
-
|
|
360
|
-
const { currentUser } = useContext(UserContext);
|
|
353
|
+
### Let's explore some additional examples.
|
|
361
354
|
|
|
362
|
-
|
|
363
|
-
};
|
|
355
|
+
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.
|
|
364
356
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
<UserProvider>
|
|
368
|
-
<Component />
|
|
369
|
-
</UserProvider>
|
|
370
|
-
);
|
|
371
|
-
};
|
|
357
|
+
```ts
|
|
358
|
+
const [fragment] = useCombinedHook(({ propA, propD }) => ({ propA, propD }));
|
|
372
359
|
```
|
|
373
360
|
|
|
374
|
-
|
|
361
|
+
Lastly, you have the flexibility to continue combining getters if desired. This means you can extend the functionality of combined hooks by adding more getters to merge additional states. By combining getters 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.
|
|
375
362
|
|
|
376
|
-
Let's
|
|
363
|
+
Let's see an example:
|
|
377
364
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}).getHook();
|
|
365
|
+
```ts
|
|
366
|
+
const [useCombinedHook, combinedGetter1, dispose1] = combineAsyncGetters(
|
|
367
|
+
{
|
|
368
|
+
selector: ([state1, state2]) => ({
|
|
369
|
+
...state1,
|
|
370
|
+
...state2,
|
|
371
|
+
}),
|
|
372
|
+
},
|
|
373
|
+
getter1,
|
|
374
|
+
getter2
|
|
375
|
+
);
|
|
390
376
|
|
|
391
|
-
|
|
392
|
-
|
|
377
|
+
const [useHook3, getter3, setter3] = createGlobalStateWithDecoupledFuncs({
|
|
378
|
+
propE: 1,
|
|
379
|
+
propF: 2,
|
|
380
|
+
});
|
|
393
381
|
|
|
394
|
-
const
|
|
395
|
-
|
|
382
|
+
const [useIsLoading, isLoadingGetter, isLoadingSetter] =
|
|
383
|
+
createGlobalStateWithDecoupledFuncs(false);
|
|
384
|
+
```
|
|
396
385
|
|
|
397
|
-
|
|
398
|
-
const [count, setCount] = useCountStore();
|
|
386
|
+
Once we created another peace of state, we can combine it with our other **global hooks** and **emitters**
|
|
399
387
|
|
|
400
|
-
|
|
401
|
-
|
|
388
|
+
```ts
|
|
389
|
+
const [useCombinedHook2, combinedGetter2, dispose2] = combineAsyncGetters(
|
|
390
|
+
{
|
|
391
|
+
selector: ([state1, state2, isLoading]) => ({
|
|
392
|
+
...state1,
|
|
393
|
+
...state2,
|
|
394
|
+
isLoading,
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
combinedGetter1,
|
|
398
|
+
getter3,
|
|
399
|
+
isLoadingGetter
|
|
400
|
+
);
|
|
402
401
|
```
|
|
403
402
|
|
|
404
|
-
|
|
403
|
+
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.
|
|
405
404
|
|
|
406
|
-
### **
|
|
405
|
+
### **Quick note**:
|
|
407
406
|
|
|
408
|
-
|
|
409
|
-
type TUser = {
|
|
410
|
-
name: string;
|
|
411
|
-
email: string;
|
|
412
|
-
};
|
|
407
|
+
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.
|
|
413
408
|
|
|
414
|
-
|
|
415
|
-
currentUser: TUser;
|
|
416
|
-
}>({
|
|
417
|
-
currentUser: null,
|
|
418
|
-
});
|
|
409
|
+
## Setter
|
|
419
410
|
|
|
420
|
-
|
|
421
|
-
const CountContext = createContext({
|
|
422
|
-
count: 0,
|
|
423
|
-
setCount: (() => {
|
|
424
|
-
throw new Error('not implemented');
|
|
425
|
-
}) as Dispatch<SetStateAction<number>>,
|
|
426
|
-
});
|
|
411
|
+
Similarly, the **contactsSetter** method allows you to modify the state stored in **useContacts**. You can use this method to update the state with a new value or perform any necessary state mutations without the restrictions imposed by **hooks**.
|
|
427
412
|
|
|
428
|
-
|
|
429
|
-
const [currentUser, setCurrentUser] = useState<TUser>(null);
|
|
413
|
+
These additional methods provide a more flexible and granular way to interact with the state managed by **useContacts**. You can retrieve and modify the state as needed, without establishing a subscription relationship or reactivity with the state changes.
|
|
430
414
|
|
|
431
|
-
|
|
415
|
+
Let's add more actions to the state and explore how to use one action from inside another.
|
|
432
416
|
|
|
433
|
-
|
|
434
|
-
<UserContext.Provider value={{ currentUser }}>
|
|
435
|
-
{children}
|
|
436
|
-
</UserContext.Provider>
|
|
437
|
-
);
|
|
438
|
-
};
|
|
417
|
+
Here's an example of adding multiple actions to the state and utilizing one action within another:
|
|
439
418
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const [count, setCount] = useState(0);
|
|
419
|
+
```ts
|
|
420
|
+
import { createGlobalState } from 'react-global-state-hooks';
|
|
443
421
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
};
|
|
422
|
+
export const useCount = createGlobalState(0, {
|
|
423
|
+
actions: {
|
|
424
|
+
log: (currentValue: string) => {
|
|
425
|
+
return ({ getState }: StoreTools<number>): void => {
|
|
426
|
+
console.log(`Current Value: ${getState()}`);
|
|
427
|
+
};
|
|
428
|
+
},
|
|
450
429
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
<UserProvider>
|
|
455
|
-
<CountProvider>
|
|
456
|
-
<Component />
|
|
457
|
-
</CountProvider>
|
|
458
|
-
</UserProvider>
|
|
459
|
-
);
|
|
460
|
-
};
|
|
430
|
+
increase(value: number = 1) {
|
|
431
|
+
return ({ getState, setState, actions }: StoreTools<number>) => {
|
|
432
|
+
setState((count) => count + value);
|
|
461
433
|
|
|
462
|
-
|
|
463
|
-
|
|
434
|
+
actions.log(message);
|
|
435
|
+
};
|
|
436
|
+
},
|
|
464
437
|
|
|
465
|
-
|
|
466
|
-
|
|
438
|
+
decrease(value: number = 1) {
|
|
439
|
+
return ({ getState, setState, actions }: StoreTools<number>) => {
|
|
440
|
+
setState((count) => count - value);
|
|
467
441
|
|
|
468
|
-
|
|
469
|
-
};
|
|
442
|
+
actions.log(message);
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
} as const,
|
|
446
|
+
});
|
|
470
447
|
```
|
|
471
448
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
### 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?
|
|
449
|
+
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.
|
|
475
450
|
|
|
476
|
-
|
|
451
|
+
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:
|
|
477
452
|
|
|
478
|
-
|
|
453
|
+
```ts
|
|
454
|
+
import { createGlobalStateWithDecoupledFuncs } from 'react-global-state-hooks';
|
|
455
|
+
|
|
456
|
+
export const [useCount, getCount, $actions] =
|
|
457
|
+
createGlobalStateWithDecoupledFuncs(0, {
|
|
458
|
+
actions: {
|
|
459
|
+
log: (currentValue: string) => {
|
|
460
|
+
return ({ getState }: StoreTools<number>): void => {
|
|
461
|
+
console.log(`Current Value: ${getState()}`);
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
increase(value: number = 1) {
|
|
466
|
+
return ({ getState, setState }: StoreTools<number>) => {
|
|
467
|
+
setState((count) => count + value);
|
|
468
|
+
|
|
469
|
+
$actions.log(message);
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
} as const,
|
|
473
|
+
});
|
|
474
|
+
```
|
|
479
475
|
|
|
480
|
-
|
|
481
|
-
type TUser = {
|
|
482
|
-
name: string;
|
|
483
|
-
email: string;
|
|
484
|
-
};
|
|
476
|
+
In the example the hook will work the same and you'll have access to the correct typing.
|
|
485
477
|
|
|
486
|
-
|
|
487
|
-
currentUser: TUser;
|
|
488
|
-
}>({
|
|
489
|
-
currentUser: null,
|
|
490
|
-
});
|
|
478
|
+
# Local Storage
|
|
491
479
|
|
|
492
|
-
|
|
493
|
-
const CountContext = createContext({
|
|
494
|
-
count: 0,
|
|
495
|
-
});
|
|
480
|
+
By default, our global hooks are capable of persisting information in local storage. To achieve this, you need to provide the key that will be used to persist the data. Additionally, you have the option to encrypt the stored data.
|
|
496
481
|
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
decrease: (): void => {
|
|
503
|
-
throw new Error('decrease is not implemented');
|
|
482
|
+
```ts
|
|
483
|
+
const useContacts = createGlobalState(
|
|
484
|
+
{
|
|
485
|
+
filter: '',
|
|
486
|
+
items: [] as Contact[],
|
|
504
487
|
},
|
|
505
|
-
|
|
488
|
+
{
|
|
489
|
+
localStorage: {
|
|
490
|
+
key: 'data',
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
```
|
|
506
495
|
|
|
507
|
-
|
|
508
|
-
const [currentUser, setCurrentUser] = useState<TUser>(null);
|
|
496
|
+
That means your data will automatically be synchronized with the local storage, and you don't need to worry about losing the type of your Maps/Sets/Dates - the store takes care of it for you.
|
|
509
497
|
|
|
510
|
-
|
|
498
|
+
# Extending Global Hooks
|
|
511
499
|
|
|
512
|
-
|
|
513
|
-
<UserContext.Provider value={{ currentUser }}>
|
|
514
|
-
{children}
|
|
515
|
-
</UserContext.Provider>
|
|
516
|
-
);
|
|
517
|
-
};
|
|
500
|
+
Creating a custom builder for your **global hook** is made incredibly easy with the **createCustomGlobalState** function.
|
|
518
501
|
|
|
519
|
-
|
|
520
|
-
const CountProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|
521
|
-
const [count, setCount] = useState(0);
|
|
522
|
-
|
|
523
|
-
const increase = () => setCount(count + 1);
|
|
524
|
-
const decrease = () => setCount(count - 1);
|
|
525
|
-
|
|
526
|
-
return (
|
|
527
|
-
//one context is gonna share the edition of the state
|
|
528
|
-
<CountContext.Provider value={{ count }}>
|
|
529
|
-
{/* this second component will share the mutations of the state */}
|
|
530
|
-
<CountContextSetter.Provider value={{ increase, decrease }}>
|
|
531
|
-
{children}
|
|
532
|
-
</CountContextSetter.Provider>
|
|
533
|
-
</CountContext.Provider>
|
|
534
|
-
);
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
// 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**
|
|
538
|
-
const App = () => {
|
|
539
|
-
return (
|
|
540
|
-
<UserProvider>
|
|
541
|
-
<CountProvider>
|
|
542
|
-
{/* lets create two componets instead of one */}
|
|
543
|
-
<ComponentSetter />
|
|
544
|
-
<Component />
|
|
545
|
-
</CountProvider>
|
|
546
|
-
</UserProvider>
|
|
547
|
-
);
|
|
548
|
-
};
|
|
502
|
+
This function returns a new global state builder wrapped with the desired custom implementation, allowing you to get creative! Le'ts see and example:
|
|
549
503
|
|
|
550
|
-
|
|
551
|
-
|
|
504
|
+
```ts
|
|
505
|
+
import { formatFromStore, formatToStore, createCustomGlobalState } = 'react-global-state-hooks'
|
|
552
506
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
<button onPress={decrease}>Decrease</button>
|
|
557
|
-
</>
|
|
558
|
-
);
|
|
507
|
+
// Optional configuration available for the consumers of the builder
|
|
508
|
+
type HookConfig = {
|
|
509
|
+
asyncStorageKey?: string;
|
|
559
510
|
};
|
|
560
511
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
// finally we are able to get access to the new context...
|
|
565
|
-
const { count } = useContext(CountContext);
|
|
566
|
-
|
|
567
|
-
return (
|
|
568
|
-
<>
|
|
569
|
-
<label>{currentUser.name}</label>
|
|
570
|
-
<label>{count}</label>
|
|
571
|
-
</>
|
|
572
|
-
);
|
|
512
|
+
// This is the base metadata that all the stores created from the builder will have.
|
|
513
|
+
type BaseMetadata = {
|
|
514
|
+
isAsyncStorageReady?: boolean;
|
|
573
515
|
};
|
|
574
|
-
```
|
|
575
516
|
|
|
576
|
-
|
|
517
|
+
export const createGlobalState = createCustomGlobalState<
|
|
518
|
+
BaseMetadata,
|
|
519
|
+
HookConfig
|
|
520
|
+
>({
|
|
521
|
+
/**
|
|
522
|
+
* This function executes immediately after the global state is created, before the invocations of the hook
|
|
523
|
+
*/
|
|
524
|
+
onInitialize: async ({ setState, setMetadata }, config) => {
|
|
525
|
+
setMetadata((metadata) => ({
|
|
526
|
+
...(metadata ?? {}),
|
|
527
|
+
isAsyncStorageReady: null,
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
const asyncStorageKey = config?.asyncStorageKey;
|
|
531
|
+
if (!asyncStorageKey) return;
|
|
532
|
+
|
|
533
|
+
const storedItem = (await asyncStorage.getItem(asyncStorageKey)) as string;
|
|
534
|
+
|
|
535
|
+
// update the metadata, remember, metadata is not reactive
|
|
536
|
+
setMetadata((metadata) => ({
|
|
537
|
+
...metadata,
|
|
538
|
+
isAsyncStorageReady: true,
|
|
539
|
+
}));
|
|
540
|
+
|
|
541
|
+
if (storedItem === null) {
|
|
542
|
+
return setState((state) => state, { forceUpdate: true });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const parsed = formatFromStore(storedItem, {
|
|
546
|
+
jsonParse: true,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
setState(parsed, { forceUpdate: true });
|
|
550
|
+
},
|
|
577
551
|
|
|
578
|
-
|
|
552
|
+
onChange: ({ getState }, config) => {
|
|
553
|
+
if (!config?.asyncStorageKey) return;
|
|
579
554
|
|
|
580
|
-
|
|
581
|
-
type TUser = {
|
|
582
|
-
name: string;
|
|
583
|
-
email: string;
|
|
584
|
-
};
|
|
555
|
+
const state = getState();
|
|
585
556
|
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}).getHook();
|
|
590
|
-
|
|
591
|
-
// let's modify the store to add custom actions, the second parameter is configuration let's just pass null for now
|
|
592
|
-
const countStore = new GlobalStore(0, null, {
|
|
593
|
-
increase() {
|
|
594
|
-
return ({ setState }: StoreTools<number>) => {
|
|
595
|
-
setState((state) => state + 1);
|
|
596
|
-
};
|
|
597
|
-
},
|
|
557
|
+
const formattedObject = formatToStore(state, {
|
|
558
|
+
stringify: true,
|
|
559
|
+
});
|
|
598
560
|
|
|
599
|
-
|
|
600
|
-
return ({ setState }: StoreTools<number>) => {
|
|
601
|
-
setState((state) => state - 1);
|
|
602
|
-
};
|
|
561
|
+
asyncStorage.setItem(config.asyncStorageKey, formattedObject);
|
|
603
562
|
},
|
|
604
|
-
}
|
|
563
|
+
});
|
|
564
|
+
```
|
|
605
565
|
|
|
606
|
-
|
|
566
|
+
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.
|
|
607
567
|
|
|
608
|
-
|
|
609
|
-
const [, countActions] = countStore.getHookDecoupled();
|
|
568
|
+
It is worth mentioning that the **onInitialize** function will be executed only once per global state.
|
|
610
569
|
|
|
611
|
-
|
|
612
|
-
const ComponentSetter = () => {
|
|
613
|
-
return (
|
|
614
|
-
<>
|
|
615
|
-
<button onPress={countActions.increase}>Increase</button>
|
|
616
|
-
<button onPress={countActions.decrease}>Decrease</button>
|
|
617
|
-
</>
|
|
618
|
-
);
|
|
619
|
-
};
|
|
570
|
+
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).
|
|
620
571
|
|
|
621
|
-
|
|
622
|
-
const Component = () => {
|
|
623
|
-
const [user] = useUser();
|
|
624
|
-
const [count, actions] = useCount();
|
|
572
|
+
Let's see how to create a global state using our new builder:
|
|
625
573
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
};
|
|
574
|
+
```ts
|
|
575
|
+
const useTodos = createGlobalState(new Map<string, number>(), {
|
|
576
|
+
config: {
|
|
577
|
+
asyncStorageKey: 'todos',
|
|
578
|
+
},
|
|
579
|
+
});
|
|
632
580
|
```
|
|
633
581
|
|
|
634
|
-
|
|
582
|
+
That's correct! If you add an **asyncStorageKey** to the state configuration, the state will be synchronized with the **asyncStorage**
|
|
635
583
|
|
|
636
|
-
|
|
584
|
+
Let's see how to use this async storage hook into our components:
|
|
637
585
|
|
|
638
586
|
```ts
|
|
639
|
-
const
|
|
640
|
-
log: (action: string) => () => console.log(action),
|
|
587
|
+
const [todos, setTodos, metadata] = useTodos();
|
|
641
588
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
589
|
+
return (<>
|
|
590
|
+
{metadata.isAsyncStorageReady ? <TodoList todos={todos} /> : <Text>Loading...</Text>}
|
|
591
|
+
<>);
|
|
592
|
+
```
|
|
646
593
|
|
|
647
|
-
|
|
648
|
-
this.log('increase');
|
|
594
|
+
The **metadata** is not reactive information and can only be modified from inside the global state lifecycle methods.
|
|
649
595
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
} as const);
|
|
596
|
+
# Life cycle methods
|
|
597
|
+
|
|
598
|
+
There are some lifecycle methods available for use with global hooks, let's review them:
|
|
654
599
|
|
|
655
|
-
|
|
600
|
+
```ts
|
|
601
|
+
/**
|
|
602
|
+
* @description callback function called when the store is initialized
|
|
603
|
+
* @returns {void} result - void
|
|
604
|
+
* */
|
|
605
|
+
onInit?: ({
|
|
606
|
+
/**
|
|
607
|
+
* Set the metadata
|
|
608
|
+
* @param {TMetadata} setter - The metadata or a function that will receive the metadata and return the new metadata
|
|
609
|
+
* */
|
|
610
|
+
setMetadata: MetadataSetter<TMetadata>;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Set the state
|
|
614
|
+
* @param {TState} setter - The state or a function that will receive the state and return the new state
|
|
615
|
+
* @param {{ forceUpdate?: boolean }} options - Options
|
|
616
|
+
* */
|
|
617
|
+
setState: StateSetter<TState>;
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Get the state
|
|
621
|
+
* @returns {TState} result - The state
|
|
622
|
+
* */
|
|
623
|
+
getState: () => TState;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Get the metadata
|
|
627
|
+
* @returns {TMetadata} result - The metadata
|
|
628
|
+
* */
|
|
629
|
+
getMetadata: () => TMetadata;
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Actions of the hook if configuration was provided
|
|
633
|
+
*/
|
|
634
|
+
actions: TActions;
|
|
635
|
+
}: StateConfigCallbackParam<TState, TMetadata, TActions>) => void;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* @description - callback function called every time the state is changed
|
|
639
|
+
*/
|
|
640
|
+
onStateChanged?: (parameters: StateChangesParam<TState, TMetadata, TActions>) => void;
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* callback function called every time a component is subscribed to the store
|
|
644
|
+
*/
|
|
645
|
+
onSubscribed?: (parameters: StateConfigCallbackParam<TState, TMetadata, TActions>) => void;
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* callback function called every time the state is about to change and it allows you to prevent the state change
|
|
649
|
+
*/
|
|
650
|
+
computePreventStateChange?: (parameters: StateChangesParam<TState, TMetadata, TActions>) => boolean;
|
|
656
651
|
```
|
|
657
652
|
|
|
658
|
-
|
|
653
|
+
You can pass this callbacks between on the second parameter of the builders like **createGlobalState**
|
|
659
654
|
|
|
660
655
|
```ts
|
|
661
|
-
const
|
|
656
|
+
const useData = createGlobalState(
|
|
657
|
+
{ value: 1 },
|
|
658
|
+
{
|
|
659
|
+
metadata: {
|
|
660
|
+
someExtraInformation: 'someExtraInformation',
|
|
661
|
+
},
|
|
662
|
+
// onSubscribed: (StateConfigCallbackParam) => {},
|
|
663
|
+
// onInit // etc
|
|
664
|
+
computePreventStateChange: ({ state, previousState }) => {
|
|
665
|
+
const prevent = isEqual(state, previousState);
|
|
662
666
|
|
|
663
|
-
|
|
664
|
-
|
|
667
|
+
return prevent;
|
|
668
|
+
},
|
|
669
|
+
}
|
|
670
|
+
);
|
|
665
671
|
```
|
|
666
672
|
|
|
667
|
-
|
|
673
|
+
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.
|
|
668
674
|
|
|
669
|
-
|
|
675
|
+
Let's see an example again with the **asyncStorage** custom global hook but with the abstract class.
|
|
670
676
|
|
|
671
|
-
|
|
672
|
-
|
|
677
|
+
```ts
|
|
678
|
+
export class GlobalStore<
|
|
679
|
+
TState,
|
|
680
|
+
TMetadata extends {
|
|
681
|
+
asyncStorageKey?: string;
|
|
682
|
+
isAsyncStorageReady?: boolean;
|
|
683
|
+
} | null = null,
|
|
684
|
+
TStateSetter extends
|
|
685
|
+
| ActionCollectionConfig<TState, TMetadata>
|
|
686
|
+
| StateSetter<TState> = StateSetter<TState>
|
|
687
|
+
> extends GlobalStoreAbstract<TState, TMetadata, TStateSetter> {
|
|
688
|
+
constructor(
|
|
689
|
+
state: TState,
|
|
690
|
+
config: GlobalStoreConfig<TState, TMetadata, TStateSetter> = {},
|
|
691
|
+
actionsConfig: TStateSetter | null = null
|
|
692
|
+
) {
|
|
693
|
+
super(state, config, actionsConfig);
|
|
694
|
+
|
|
695
|
+
this.initialize();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
protected onInitialize = async ({
|
|
699
|
+
setState,
|
|
700
|
+
setMetadata,
|
|
701
|
+
getMetadata,
|
|
702
|
+
getState,
|
|
703
|
+
}: StateConfigCallbackParam<TState, TMetadata, TStateSetter>) => {
|
|
704
|
+
setMetadata({
|
|
705
|
+
...(metadata ?? {}),
|
|
706
|
+
isAsyncStorageReady: null,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const metadata = getMetadata();
|
|
710
|
+
const asyncStorageKey = metadata?.asyncStorageKey;
|
|
711
|
+
|
|
712
|
+
if (!asyncStorageKey) return;
|
|
713
|
+
|
|
714
|
+
const storedItem = (await asyncStorage.getItem(asyncStorageKey)) as string;
|
|
715
|
+
setMetadata({
|
|
716
|
+
...metadata,
|
|
717
|
+
isAsyncStorageReady: true,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
if (storedItem === null) {
|
|
721
|
+
const state = getState();
|
|
722
|
+
|
|
723
|
+
// force the re-render of the subscribed components even if the state is the same
|
|
724
|
+
return setState(state, { forceUpdate: true });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const items = formatFromStore<TState>(storedItem, {
|
|
728
|
+
jsonParse: true,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
setState(items, { forceUpdate: true });
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
protected onChange = ({
|
|
735
|
+
getMetadata,
|
|
736
|
+
getState,
|
|
737
|
+
}: StateChangesParam<TState, TMetadata, NonNullable<TStateSetter>>) => {
|
|
738
|
+
const asyncStorageKey = getMetadata()?.asyncStorageKey;
|
|
739
|
+
|
|
740
|
+
if (!asyncStorageKey) return;
|
|
673
741
|
|
|
674
|
-
|
|
742
|
+
const state = getState();
|
|
675
743
|
|
|
676
|
-
|
|
744
|
+
const formattedObject = formatToStore(state, {
|
|
745
|
+
stringify: true,
|
|
746
|
+
});
|
|
677
747
|
|
|
678
|
-
|
|
679
|
-
|
|
748
|
+
asyncStorage.setItem(asyncStorageKey, formattedObject);
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
Then, from an instance of the global store, you will be able to access the hooks.
|
|
754
|
+
|
|
755
|
+
```ts
|
|
756
|
+
const storage = new GlobalStore(0, {
|
|
680
757
|
metadata: {
|
|
681
|
-
|
|
758
|
+
asyncStorageKey: 'counter',
|
|
759
|
+
isAsyncStorageReady: false,
|
|
682
760
|
},
|
|
683
|
-
})
|
|
761
|
+
});
|
|
684
762
|
|
|
685
|
-
|
|
763
|
+
const [getState, _, getMetadata] = storage.getHookDecoupled();
|
|
764
|
+
const useState = storage.getHook();
|
|
686
765
|
```
|
|
687
766
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
...
|
|
691
|
-
|
|
692
|
-
...
|
|
767
|
+
### **Note**: The GlobalStore class is still available in the package in case you were already extending from it.
|
|
693
768
|
|
|
694
769
|
# That's it for now!! hope you enjoy coding!!
|