react-redux-cache 0.22.2 → 0.22.4
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 +6 -595
- package/dist/cjs/utilsAndConstants.js +3 -1
- package/dist/esm/utilsAndConstants.js +3 -1
- package/dist/types/types.d.ts +6 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,598 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
<summary>Donations 🙌</summary>
|
|
3
|
-
<b>BTC:</b> bc1qs0sq7agz5j30qnqz9m60xj4tt8th6aazgw7kxr <br>
|
|
4
|
-
<b>ETH:</b> 0x1D834755b5e889703930AC9b784CB625B3cd833E <br>
|
|
5
|
-
<b>USDT(Tron):</b> TPrCq8LxGykQ4as3o1oB8V7x1w2YPU2o5n <br>
|
|
6
|
-
<b>TON:</b> EQAtBuFWI3H_LpHfEToil4iYemtfmyzlaJpahM3tFSoxojvV <br>
|
|
7
|
-
<b>DOGE:</b> D7GMQdKhKC9ymbT9PtcetSFTQjyPRRfkwT <br>
|
|
8
|
-
</details>
|
|
1
|
+
# DEPRECATED - react-redux-cache
|
|
9
2
|
|
|
10
|
-
|
|
3
|
+
**⚠️ This package has been renamed**
|
|
11
4
|
|
|
12
|
-
|
|
5
|
+
Please use **[RRC](https://www.npmjs.com/package/rrc)** instead.
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|Principle|Description|
|
|
19
|
-
|--|--|
|
|
20
|
-
|Full access to the store|You choose the store (redux / zustand) and have full access to state, reducer, actions, hooks, selectors and utils, used by this library, and can create your own.|
|
|
21
|
-
|Supports all kinds of queries / mutations|REST, GraphQL, databases - any async operations can be cached.|
|
|
22
|
-
|Fully typed|Written on TypeScript, everything is checked by compiler.|
|
|
23
|
-
|Not overengineered|Simplicity is the main goal.|
|
|
24
|
-
|Performance|Every function is heavily optimized. Immer is not used ([RTK performance issue](https://github.com/reduxjs/redux-toolkit/issues/4793)). Supports mutable collections (O(n) > O(1)).|
|
|
25
|
-
|Reliability|High test coverage, zero issue policy.|
|
|
26
|
-
|Lightweight|`npx minified-size dist/esm/*.js`<br/>minified: 19.1 kB<br/>gzipped: 8.29 kB<br/>brotlied: 7.41 kB
|
|
27
|
-
|
|
28
|
-
|Feature|Description|
|
|
29
|
-
|--|--|
|
|
30
|
-
|De-duplication of queries / mutations|Similar parallel queries are combined into one, mutations - aborted.|
|
|
31
|
-
|Time to live & Invalidation|Choose how long query result can be used before expired, or clear / invalidate it manually.|
|
|
32
|
-
|Deep comparison|Rendering is much heavier than deep comparison of incoming data, so it is enabled by default to prevent excess renders.|
|
|
33
|
-
|Infinite pagination|Easily implemented - one or two direction.|
|
|
34
|
-
|Error handling|No need to use try / catch, errors are returned by fuctions and / or can be handled globally from single place.|
|
|
35
|
-
|Fetch policies|Decide if data is full enough or need to be fetched.|
|
|
36
|
-
|Normalization|Consistent state accross the app - better UX, minimum loading states and lower traffic consumption.|
|
|
37
|
-
|Minimal state|Default values such as `undefined` or default query states are removed from the state tree.|
|
|
38
|
-
|BETA: Mutable collections|Optimizes state merges from O(n) to O(1) by using mutable collections. Separate entities, query and mutation states are still immutable.|
|
|
39
|
-
|
|
40
|
-
#### Examples of states, generated by cache reducer from `/example` project:
|
|
41
|
-
<details>
|
|
42
|
-
<summary>
|
|
43
|
-
Normalized
|
|
44
|
-
</summary>
|
|
45
|
-
|
|
46
|
-
```js
|
|
47
|
-
{
|
|
48
|
-
entities: {
|
|
49
|
-
// each typename has its own map of entities, stored by id
|
|
50
|
-
users: {
|
|
51
|
-
"0": {id: 0, bankId: "0", name: "User 0 *"},
|
|
52
|
-
"1": {id: 1, bankId: "1", name: "User 1 *"},
|
|
53
|
-
"2": {id: 2, bankId: "2", name: "User 2"},
|
|
54
|
-
"3": {id: 3, bankId: "3", name: "User 3"}
|
|
55
|
-
},
|
|
56
|
-
banks: {
|
|
57
|
-
"0": {id: "0", name: "Bank 0"},
|
|
58
|
-
"1": {id: "1", name: "Bank 1"},
|
|
59
|
-
"2": {id: "2", name: "Bank 2"},
|
|
60
|
-
"3": {id: "3", name: "Bank 3"}
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
queries: {
|
|
64
|
-
// each query has its own map of query states, stored by cache key, which is generated from query params
|
|
65
|
-
getUser: {
|
|
66
|
-
"2": {result: 2, params: 2, expiresAt: 1727217298025},
|
|
67
|
-
"3": {loading: Promise<...>, params: 3}
|
|
68
|
-
},
|
|
69
|
-
getUsers: {
|
|
70
|
-
// example of paginated state under custom cache key
|
|
71
|
-
"feed": {
|
|
72
|
-
result: {items: [0,1,2], page: 1},
|
|
73
|
-
params: {page: 1}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
mutations: {
|
|
78
|
-
// each mutation has its own state as well
|
|
79
|
-
updateUser: {
|
|
80
|
-
result: 1,
|
|
81
|
-
params: {id: 1, name: "User 1 *"}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
</details>
|
|
87
|
-
|
|
88
|
-
<details>
|
|
89
|
-
<summary>
|
|
90
|
-
Not normalized
|
|
91
|
-
</summary>
|
|
92
|
-
|
|
93
|
-
```js
|
|
94
|
-
{
|
|
95
|
-
// entities map is used for normalization and is empty here
|
|
96
|
-
entities: {},
|
|
97
|
-
queries: {
|
|
98
|
-
// each query has its own map of query states, stored by cache key, which is generated from query params
|
|
99
|
-
getUser: {
|
|
100
|
-
"2": {
|
|
101
|
-
result: {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"},
|
|
102
|
-
params: 2,
|
|
103
|
-
expiresAt: 1727217298025
|
|
104
|
-
},
|
|
105
|
-
"3": {loading: Promise<...>, params: 3}
|
|
106
|
-
},
|
|
107
|
-
getUsers: {
|
|
108
|
-
// example of paginated state under custom cache key
|
|
109
|
-
"feed": {
|
|
110
|
-
result: {
|
|
111
|
-
items: [
|
|
112
|
-
{id: 0, bank: {id: "0", name: "Bank 0"}, name: "User 0 *"},
|
|
113
|
-
{id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
|
|
114
|
-
{id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"}
|
|
115
|
-
],
|
|
116
|
-
page: 1
|
|
117
|
-
},
|
|
118
|
-
params: {page: 1}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
mutations: {
|
|
123
|
-
// each mutation has its own state as well
|
|
124
|
-
updateUser: {
|
|
125
|
-
result: {id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
|
|
126
|
-
params: {id: 1, name: "User 1 *"}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
</details>
|
|
132
|
-
|
|
133
|
-
[API Reference](DOCUMENTATION.md)
|
|
134
|
-
|
|
135
|
-
### Table of contents
|
|
136
|
-
|
|
137
|
-
- [Installation](https://github.com/gentlee/react-redux-cache#Installation)
|
|
138
|
-
- [Initialization](https://github.com/gentlee/react-redux-cache#Initialization)
|
|
139
|
-
- [cache.ts](https://github.com/gentlee/react-redux-cache#cachets)
|
|
140
|
-
- [store.ts](https://github.com/gentlee/react-redux-cache#storets)
|
|
141
|
-
- [api.ts](https://github.com/gentlee/react-redux-cache#apits)
|
|
142
|
-
- [Usage](https://github.com/gentlee/react-redux-cache#usage)
|
|
143
|
-
- [Advanced](https://github.com/gentlee/react-redux-cache#advanced)
|
|
144
|
-
- [Mutable collections](https://github.com/gentlee/react-redux-cache#mutable-collections)
|
|
145
|
-
- [Error handling](https://github.com/gentlee/react-redux-cache#error-handling)
|
|
146
|
-
- [Invalidation](https://github.com/gentlee/react-redux-cache#invalidation)
|
|
147
|
-
- [Extended & custom fetch policy](https://github.com/gentlee/react-redux-cache#extended--custom-fetch-policy)
|
|
148
|
-
- [Infinite scroll pagination](https://github.com/gentlee/react-redux-cache#infinite-scroll-pagination)
|
|
149
|
-
- [redux-persist](https://github.com/gentlee/react-redux-cache#redux-persist)
|
|
150
|
-
- [FAQ](https://github.com/gentlee/react-redux-cache#faq)
|
|
151
|
-
- [What is a query cache key?](https://github.com/gentlee/react-redux-cache#what-is-a-query-cache-key)
|
|
152
|
-
- [How race conditions are handled?](https://github.com/gentlee/react-redux-cache#how-race-conditions-are-handled)
|
|
153
|
-
|
|
154
|
-
### Installation
|
|
155
|
-
`react` is a peer dependency.
|
|
156
|
-
|
|
157
|
-
`react-redux` and `fast-deep-equal` are optional peer dependencies:
|
|
158
|
-
- `react-redux` required when `storeHooks` is not provided when creating cache. Not needed for Zustand.
|
|
159
|
-
- `fast-deep-equal` required if `deepComparisonEnabled` cache option is enabled (default is true).
|
|
160
|
-
|
|
161
|
-
```sh
|
|
162
|
-
# required
|
|
163
|
-
npm i react-redux-cache react
|
|
164
|
-
|
|
165
|
-
# without react-redux
|
|
166
|
-
npm i react-redux-cache react fast-deep-equal
|
|
167
|
-
|
|
168
|
-
# all required and optional peers
|
|
169
|
-
npm i react-redux-cache react react-redux fast-deep-equal
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Initialization
|
|
173
|
-
The only function that needs to be imported is either `withTypenames`, which is needed for normalization, or directly `createCache` if it is not needed. `createCache` creates fully typed reducer, hooks, actions, selectors and utils to be used in the app. You can create as many caches as needed, but keep in mind that normalization is not shared between them.
|
|
174
|
-
All queries and mutations should be passed while initializing the cache for proper typing.
|
|
175
|
-
|
|
176
|
-
#### cache.ts
|
|
177
|
-
|
|
178
|
-
> Zustand requires additional option - `storeHooks`.
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
// Mapping of all typenames to their entity types, which is needed for proper normalization typing.
|
|
182
|
-
// Not needed if normalization is not used.
|
|
183
|
-
export type CacheTypenames = {
|
|
184
|
-
users: User, // here `users` entities will have type `User`
|
|
185
|
-
banks: Bank,
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// `withTypenames` is only needed to provide proper Typenames for normalization - limitation of Typescript.
|
|
189
|
-
// `createCache` can be imported directly without `withTypenames`.
|
|
190
|
-
export const {
|
|
191
|
-
cache,
|
|
192
|
-
reducer,
|
|
193
|
-
hooks: {useClient, useMutation, useQuery},
|
|
194
|
-
} = withTypenames<CacheTypenames>().createCache({
|
|
195
|
-
name: 'cache', // Used as prefix for actions and in default cacheStateSelector for selecting cache state from redux state.
|
|
196
|
-
queries: {
|
|
197
|
-
getUsers: { query: getUsers },
|
|
198
|
-
getUser: {
|
|
199
|
-
query: getUser,
|
|
200
|
-
// For each query `secondsToLive` option can be set, which is used to set expiration date of a cached result when query response is received.
|
|
201
|
-
// After expiration query result is considered invalidated and will be refetched on the next useQuery mount.
|
|
202
|
-
// Can also be set globally in `globals`.
|
|
203
|
-
secondsToLive: 5 * 60 // Here cached result is valid for 5 minutes.
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
mutations: {
|
|
207
|
-
updateUser: { mutation: updateUser },
|
|
208
|
-
removeUser: { mutation: removeUser },
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
// Required for Zustand. Just an empty object can be passed during initialization, and hooks can be set later (see `store.ts` section).
|
|
212
|
-
// Can be also used for Redux if working with multiple stores.
|
|
213
|
-
storeHooks: {},
|
|
214
|
-
})
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
For normalization two things are required:
|
|
218
|
-
- Set proper typenames while creating the cache - mapping of all entities and their corresponding TS types.
|
|
219
|
-
- Return an object from queries and mutations that contains the following fields (besides `result`):
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
type EntityChanges<T extends Typenames> = {
|
|
223
|
-
merge?: PartialEntitiesMap<T> /** Entities that will be merged with existing. */
|
|
224
|
-
replace?: Partial<EntitiesMap<T>> /** Entities that will replace existing. */
|
|
225
|
-
remove?: EntityIds<T> /** Ids of entities that will be removed. */
|
|
226
|
-
entities?: EntityChanges<T>['merge'] /** Alias for `merge` to support normalizr. */
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
#### store.ts
|
|
231
|
-
|
|
232
|
-
Redux:
|
|
233
|
-
```typescript
|
|
234
|
-
// Create store as usual, passing the new cache reducer under the name of the cache.
|
|
235
|
-
// If some other redux structure is needed, provide custom cacheStateSelector when creating cache.
|
|
236
|
-
const store = configureStore({
|
|
237
|
-
reducer: {
|
|
238
|
-
[cache.name]: reducer,
|
|
239
|
-
...
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Zustand:
|
|
245
|
-
```typescript
|
|
246
|
-
type State = {[cache.name]: ReturnType<typeof reducer>}
|
|
247
|
-
type Actions = {dispatch: (action: Parameters<typeof reducer>[1]) => void}
|
|
248
|
-
|
|
249
|
-
const initialState = {[cache.name]: getInitialState()}
|
|
250
|
-
|
|
251
|
-
export const useStore = create<State & Actions>((set, get) => ({
|
|
252
|
-
...initialState,
|
|
253
|
-
dispatch: (action) => set({[cache.name]: reducer(get()[cache.name], action)}),
|
|
254
|
-
}))
|
|
255
|
-
|
|
256
|
-
const store = {dispatch: useStore.getState().dispatch, getState: useStore.getState}
|
|
257
|
-
cache.storeHooks.useStore = () => store
|
|
258
|
-
cache.storeHooks.useSelector = useStore
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
#### api.ts
|
|
262
|
-
For normalization `normalizr` package is used in this example, but any other tool can be used if query result is of proper type.
|
|
263
|
-
Perfect implementation is when the backend already returns normalized data.
|
|
264
|
-
```typescript
|
|
265
|
-
|
|
266
|
-
// Example of query with normalization (recommended)
|
|
267
|
-
|
|
268
|
-
// 1. Result can be get by any way - fetch, axios etc, even with database connection. There is no limitation here.
|
|
269
|
-
// 2. `satisfies` keyword is used here for proper typing of params and returned value.
|
|
270
|
-
export const getUser = (async (id) => {
|
|
271
|
-
const response = await ...
|
|
272
|
-
|
|
273
|
-
return normalize(response, getUserSchema)
|
|
274
|
-
}) satisfies NormalizedQuery<CacheTypenames, number>
|
|
275
|
-
|
|
276
|
-
// Example of query without normalization (not recommended), with selecting access token from the store
|
|
277
|
-
|
|
278
|
-
export const getBank = (async (id, {getState}) => {
|
|
279
|
-
const token = tokenSelector(getState())
|
|
280
|
-
const result: Bank = ...
|
|
281
|
-
return {result} // result is bank object, no entities passed
|
|
282
|
-
}) satisfies Query<string>
|
|
283
|
-
|
|
284
|
-
// Example of mutation with normalization
|
|
285
|
-
|
|
286
|
-
export const removeUser = (async (id, _, abortSignal) => {
|
|
287
|
-
await ...
|
|
288
|
-
return {
|
|
289
|
-
remove: { users: [id] },
|
|
290
|
-
}
|
|
291
|
-
}) satisfies NormalizedQuery<CacheTypenames, number>
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### Usage
|
|
295
|
-
|
|
296
|
-
Please check `example/` folder (`npm run example` to run). There are examples for both Redux and Zustand.
|
|
297
|
-
|
|
298
|
-
#### UserScreen.tsx
|
|
299
|
-
```typescript
|
|
300
|
-
export const UserScreen = () => {
|
|
301
|
-
const {id} = useParams()
|
|
302
|
-
|
|
303
|
-
// useQuery connects to redux state and if user with that id is already cached, fetch won't happen (with default FetchPolicy.NoCacheOrExpired).
|
|
304
|
-
// Infers all types from created cache, telling here that params and result are of type `number`.
|
|
305
|
-
const [{result: userId, loading, error}] = useQuery({
|
|
306
|
-
query: 'getUser',
|
|
307
|
-
params: Number(id),
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
const [updateUser, {loading: updatingUser}] = useMutation({
|
|
311
|
-
mutation: 'updateUser',
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
// This selector returns entities with proper types - User and Bank
|
|
315
|
-
const user = useSelectEntityById(userId, 'users')
|
|
316
|
-
const bank = useSelectEntityById(user?.bankId, 'banks')
|
|
317
|
-
|
|
318
|
-
if (loading) {
|
|
319
|
-
return ...
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return ...
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Advanced
|
|
327
|
-
|
|
328
|
-
#### Mutable collections
|
|
329
|
-
|
|
330
|
-
For huge collections (> 1000 items, see benchmark) immutable approach may be a bottleneck - every merge of entity, query or mutation state is O(n). There is an option `mutableCollections` that makes it O(1) by using mutable approach when working with collections, while still keeping separate entities, query and mutation states immutable.
|
|
331
|
-
|
|
332
|
-
[Benchmark](https://github.com/gentlee/react-redux-cache/blob/main/benchmark.ts) results of adding item to collection depending on collection size, in microseconds (less is better):
|
|
333
|
-
|
|
334
|
-
| Collection size | 0 | 1000 | 10000 | 100000 | 1000000 |
|
|
335
|
-
|-|-|-|-|-|-|
|
|
336
|
-
| immutable | 1.57 | 1.81 | 7.62 | 103.82 | 1457.89 |
|
|
337
|
-
| mutable | 1.4 | 1.15 | 0.65 | 1.03 | 0.76 |
|
|
338
|
-
|
|
339
|
-
Well written code should not subcribe to whole collections, so just enabling this options most of the times should not break anything. But if it is still needed, you should subscribe to both collection (it may still change e.g. when clearing state) and to its `_changeKey`.
|
|
340
|
-
|
|
341
|
-
```tsx
|
|
342
|
-
const Component = () => {
|
|
343
|
-
// It is usually a bad idea to subscribe to whole collections, consider using order of ids and subscribe to a single entity in each cell.
|
|
344
|
-
const allUsers = useSelector((state) => selectEntitiesByTypename(state, 'users'))
|
|
345
|
-
const allUsersChangeKey = useSelector((state) => selectEntitiesByTypename(state, 'users')._changeKey) // <-- Add this line while subscribing to collections
|
|
346
|
-
|
|
347
|
-
// For memoized components you should also pass it as extra prop to cause its re-render.
|
|
348
|
-
return <List data={allUsers} extra={allUsersChangeKey}/>
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Or just use existing hook.
|
|
352
|
-
|
|
353
|
-
const Component = () => {
|
|
354
|
-
const allUsers = useEntitiesByTypename('users')
|
|
355
|
-
|
|
356
|
-
return <List data={allUsers} extra={allUsers._changeKey}>
|
|
357
|
-
}
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
#### Error handling
|
|
361
|
-
|
|
362
|
-
Queries and mutations are wrapped in try/catch, so any error will lead to cancelling of any updates to the state except loading state and the caught error. If you still want to make some state updates, or just want to use thrown errors only for unexpected cases, consider returning expected errors as a part of the result:
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
export const updateBank = (async (bank) => {
|
|
366
|
-
const {httpError, response} = ...
|
|
367
|
-
return {
|
|
368
|
-
result: {
|
|
369
|
-
httpError, // Error is a part of the result, containing e.g. map of not valid fields and threir error messages
|
|
370
|
-
bank: response?.bank // Bank still can be returned from the backend with error e.g. when only some of fields were udpated
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}) satisfies Mutation<Partial<Bank>>
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
If global error handling is needed for errors, not handled by query / mutation `onError` callback, global `onError` can be used:
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
|
-
export const cache = createCache({
|
|
380
|
-
name: 'cache',
|
|
381
|
-
globals: {
|
|
382
|
-
onError: (error, key) {
|
|
383
|
-
console.log('Not handled error', { error, key })
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
queries: {
|
|
387
|
-
getUsers: { query: getUsers },
|
|
388
|
-
},
|
|
389
|
-
...
|
|
390
|
-
})
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
#### Invalidation
|
|
394
|
-
|
|
395
|
-
`FetchPolicy.NoCacheOrExpired` (default) skips fetching on fetch triggers if result is already cached, but we can invalidate cached query results using `invalidateQuery` action to make it run again on a next mount.
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
|
|
399
|
-
export const cache = createCache({
|
|
400
|
-
...
|
|
401
|
-
mutations: {
|
|
402
|
-
updateUser: {
|
|
403
|
-
mutation: updateUser,
|
|
404
|
-
onSuccess(_, __, {dispatch}, {invalidateQuery}) {
|
|
405
|
-
// Invalidate getUsers after a single user update (can be done better by updating getUsers state with updateQueryStateAndEntities)
|
|
406
|
-
dispatch(invalidateQuery([{query: 'getUsers'}]))
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
},
|
|
410
|
-
})
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
#### Extended & custom fetch policy
|
|
414
|
-
|
|
415
|
-
Fetch policy determines if `useQuery` fetch triggers should start fetching. They are: 1) component mount 2) cache key change (=params by default) 3) `skipFetch` change to false.
|
|
416
|
-
|
|
417
|
-
`FetchPolicy.NoCacheOrExpired` (default) skips fetching if result is already cached, but sometimes it can't determine that we already have result in some other's query result or in normalized entities cache. In that case we can use `skipFetch` parameter of a query:
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
export const UserScreen = () => {
|
|
421
|
-
...
|
|
422
|
-
|
|
423
|
-
const user = useSelectEntityById(userId, 'users')
|
|
424
|
-
|
|
425
|
-
const [{loading, error}] = useQuery({
|
|
426
|
-
query: 'getUser',
|
|
427
|
-
params: userId,
|
|
428
|
-
skipFetch: !!user // Disable fetches if we already have user cached by some other query, e.g. getUsers
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
...
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
But if more control is needed, e.g. checking if entity is full, custom fetch policy can be provided:
|
|
436
|
-
|
|
437
|
-
```typescript
|
|
438
|
-
...
|
|
439
|
-
getFullUser: {
|
|
440
|
-
query: getUser,
|
|
441
|
-
fetchPolicy(expired, id, _, {getState}, {selectEntityById}) {
|
|
442
|
-
if (expired) {
|
|
443
|
-
return true // fetch if expired
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// fetch if user is not full
|
|
447
|
-
const user = selectEntityById(getState(), id, 'users')
|
|
448
|
-
return !user || !('name' in user) || !('bankId' in user)
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
...
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
One more approach is to set `skipFetch: true` by default and manually run `fetch`. `onlyIfExpired` option can be also used:
|
|
455
|
-
|
|
456
|
-
```typescript
|
|
457
|
-
export const UserScreen = () => {
|
|
458
|
-
const screenIsVisible = useScreenIsVisible()
|
|
459
|
-
|
|
460
|
-
const [{result, loading, error}, fetchUser] = useQuery({
|
|
461
|
-
query: 'getUser',
|
|
462
|
-
params: userId,
|
|
463
|
-
skipFetch: true
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
useEffect(() => {
|
|
467
|
-
if (screenIsVisible) {
|
|
468
|
-
fetchUser({ onlyIfExpired: true }) // expiration happens if expiresAt was set before e.g. by secondsToLive option or invalidateQuery action. If result is not cached yet, it is also considered as expired.
|
|
469
|
-
}
|
|
470
|
-
}, [screenIsVisible])
|
|
471
|
-
|
|
472
|
-
...
|
|
473
|
-
}
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
#### Infinite scroll pagination
|
|
477
|
-
|
|
478
|
-
Here is an example of `getUsers` query configuration with pagination support. You can check full implementation in `/example` folder.
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
// createCache
|
|
482
|
-
|
|
483
|
-
...
|
|
484
|
-
} = createCache({
|
|
485
|
-
...
|
|
486
|
-
queries: {
|
|
487
|
-
getUsers: {
|
|
488
|
-
query: getUsers,
|
|
489
|
-
getCacheKey: () => 'feed', // single cache key is used for all pages
|
|
490
|
-
mergeResults: (oldResult, {result: newResult}) => {
|
|
491
|
-
if (!oldResult || newResult.page === 1) {
|
|
492
|
-
return newResult
|
|
493
|
-
}
|
|
494
|
-
if (newResult.page === oldResult.page + 1) {
|
|
495
|
-
return {
|
|
496
|
-
...newResult,
|
|
497
|
-
items: oldResult.items.concat(newResult.items),
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return oldResult
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
...
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
// Component
|
|
508
|
-
|
|
509
|
-
export const GetUsersScreen = () => {
|
|
510
|
-
const [{result: usersResult, loading, error, params}, fetchUsers] = useQuery({
|
|
511
|
-
query: 'getUsers',
|
|
512
|
-
params: 1 // page
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
const refreshing = loading && params === 1
|
|
516
|
-
const loadingNextPage = loading && !refreshing
|
|
517
|
-
|
|
518
|
-
const onLoadNextPage = () => {
|
|
519
|
-
const lastLoadedPage = usersResult?.page ?? 0
|
|
520
|
-
fetchUsers({
|
|
521
|
-
params: lastLoadedPage + 1,
|
|
522
|
-
})
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const renderUser = (userId: number) => (
|
|
526
|
-
<UserRow key={userId} userId={userId}>
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
...
|
|
530
|
-
|
|
531
|
-
return (
|
|
532
|
-
<div>
|
|
533
|
-
{refreshing && <div className="spinner" />}
|
|
534
|
-
{usersResult?.items.map(renderUser)}
|
|
535
|
-
<button onClick={() => fetchUsers()}>Refresh</button>
|
|
536
|
-
{loadingNextPage ? (
|
|
537
|
-
<div className="spinner" />
|
|
538
|
-
) : (
|
|
539
|
-
<button onClick={loadNextPage}>Load next page</button>
|
|
540
|
-
)}
|
|
541
|
-
</div>
|
|
542
|
-
)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
#### redux-persist
|
|
548
|
-
|
|
549
|
-
Here is a simple `redux-persist` configuration:
|
|
550
|
-
|
|
551
|
-
```typescript
|
|
552
|
-
// removes `loading` and `error` from persisted state
|
|
553
|
-
function stringifyReplacer(key: string, value: unknown) {
|
|
554
|
-
return key === 'loading' || key === 'error' ? undefined : value
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const persistedReducer = persistReducer(
|
|
558
|
-
{
|
|
559
|
-
key: 'cache',
|
|
560
|
-
storage,
|
|
561
|
-
whitelist: ['entities', 'queries'], // mutations are ignored
|
|
562
|
-
throttle: 1000, // ms
|
|
563
|
-
serialize: (value: unknown) => JSON.stringify(value, stringifyReplacer),
|
|
564
|
-
},
|
|
565
|
-
reducer
|
|
566
|
-
)
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
### FAQ
|
|
570
|
-
|
|
571
|
-
#### What is a query cache key?
|
|
572
|
-
|
|
573
|
-
**Cache key** is used for storing the query state and for performing a fetch when it changes. Queries with the same cache key share their state.
|
|
574
|
-
|
|
575
|
-
Default implementation for `getCacheKey` is:
|
|
576
|
-
```typescript
|
|
577
|
-
export const defaultGetCacheKey = <P = unknown>(params: P): Key => {
|
|
578
|
-
switch (typeof params) {
|
|
579
|
-
case 'string':
|
|
580
|
-
case 'symbol':
|
|
581
|
-
return params
|
|
582
|
-
case 'object':
|
|
583
|
-
return JSON.stringify(params)
|
|
584
|
-
default:
|
|
585
|
-
return String(params)
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
It is recommended to override it when default implementation is not optimal or when keys in params object can be sorted in random order. In second case you can also consider using array to pass params.
|
|
591
|
-
|
|
592
|
-
As example, can be overridden when implementing pagination.
|
|
593
|
-
|
|
594
|
-
#### How race conditions are handled?
|
|
595
|
-
|
|
596
|
-
**Queries:** Queries are throttled: query with the same cache key (generated from params by default) is cancelled if already running.
|
|
597
|
-
|
|
598
|
-
**Mutations:** Mutations are debounced: previous similar mutation is aborted if it was running when the new one started. Third argument in mutations is `AbortSignal`, which can be used e.g. for cancelling http requests.
|
|
7
|
+
```bash
|
|
8
|
+
npm install rrc
|
|
9
|
+
```
|
|
@@ -143,7 +143,9 @@ const applyEntityChanges = (entities, changes, options) => {
|
|
|
143
143
|
if (entitiesToMerge) {
|
|
144
144
|
for (const id in entitiesToMerge) {
|
|
145
145
|
const oldEntity = oldEntities === null || oldEntities === void 0 ? void 0 : oldEntities[id]
|
|
146
|
-
const newEntity =
|
|
146
|
+
const newEntity = oldEntity
|
|
147
|
+
? Object.assign(Object.assign({}, oldEntity), entitiesToMerge[id])
|
|
148
|
+
: entitiesToMerge[id]
|
|
147
149
|
if (!(deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldEntity, newEntity))) {
|
|
148
150
|
newEntities !== null && newEntities !== void 0
|
|
149
151
|
? newEntities
|
|
@@ -129,7 +129,9 @@ export const applyEntityChanges = (entities, changes, options) => {
|
|
|
129
129
|
if (entitiesToMerge) {
|
|
130
130
|
for (const id in entitiesToMerge) {
|
|
131
131
|
const oldEntity = oldEntities === null || oldEntities === void 0 ? void 0 : oldEntities[id]
|
|
132
|
-
const newEntity =
|
|
132
|
+
const newEntity = oldEntity
|
|
133
|
+
? Object.assign(Object.assign({}, oldEntity), entitiesToMerge[id])
|
|
134
|
+
: entitiesToMerge[id]
|
|
133
135
|
if (!(deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldEntity, newEntity))) {
|
|
134
136
|
newEntities !== null && newEntities !== void 0
|
|
135
137
|
? newEntities
|
package/dist/types/types.d.ts
CHANGED
|
@@ -20,11 +20,14 @@ export type OptionalPartial<T, K extends keyof T> = Partial<{
|
|
|
20
20
|
|
|
21
21
|
/** Entity changes to be merged to the state. */
|
|
22
22
|
export type EntityChanges<T extends Typenames> = {
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* If the entity already exists, merge it. If not, add it.
|
|
25
|
+
* @warning Adding a partial entity will keep it partial in the state under full type.
|
|
26
|
+
*/
|
|
24
27
|
merge?: PartialEntitiesMap<T>
|
|
25
|
-
/**
|
|
28
|
+
/** If the entity already exists, replace it. If not, add it. */
|
|
26
29
|
replace?: Partial<EntitiesMap<T>>
|
|
27
|
-
/**
|
|
30
|
+
/** If the entity exists, remove it. If not, do nothing. */
|
|
28
31
|
remove?: EntityIds<T>
|
|
29
32
|
/** Alias for `merge` to support normalizr. */
|
|
30
33
|
entities?: EntityChanges<T>['merge']
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "react-redux-cache",
|
|
3
3
|
"author": "Alexander Danilov",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.22.
|
|
5
|
+
"version": "0.22.4",
|
|
6
6
|
"description": "Powerful data fetching and caching library for Redux and Zustand that supports normalization.",
|
|
7
7
|
"main": "./dist/cjs/index.js",
|
|
8
8
|
"module": "./dist/esm/index.js",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"example": "(cd example && yarn --production && yarn dev)",
|
|
19
19
|
"clean": "rm -rf dist",
|
|
20
|
-
"lint": "yarn eslint
|
|
21
|
-
"lint-fix": "yarn eslint --fix
|
|
22
|
-
"lint-fix-dist": "yarn eslint --quiet --fix dist/ > /dev/null 2>&1 || true",
|
|
20
|
+
"lint": "yarn eslint .",
|
|
21
|
+
"lint-fix": "yarn eslint --fix .",
|
|
22
|
+
"lint-fix-dist": "yarn eslint --quiet --no-ignore --fix dist/ > /dev/null 2>&1 || true",
|
|
23
23
|
"build-cjs": "tsc -p tsconfig.cjs.json && rm -rf dist/cjs/testing && rm -rf dist/cjs/__tests__",
|
|
24
24
|
"build-esm": "tsc -p tsconfig.esm.json > /dev/null ; rm -rf dist/esm/testing && rm -rf dist/esm/__tests__",
|
|
25
25
|
"build-types": "tsc -p tsconfig.types.json > /dev/null ; rm -rf dist/types/testing && rm -rf dist/types/__tests__",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"remove-rc": "npm version <same-version-without-rc>",
|
|
33
33
|
"prepublishOnly": "yarn build && yarn test",
|
|
34
34
|
"generate-docs": "node --experimental-strip-types scripts/generate-docs.ts",
|
|
35
|
-
"benchmark": "NODE_ENV=production node --expose-gc benchmark.mjs"
|
|
35
|
+
"benchmark": "NODE_ENV=production node --expose-gc scripts/benchmark.mjs"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"fast-deep-equal": "*",
|