react-redux-cache 0.4.3 → 0.5.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 +125 -83
- package/dist/index.js +2 -5
- package/dist/query.js +6 -13
- package/dist/types.d.ts +3 -9
- package/dist/useQuery.js +1 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,74 +15,103 @@
|
|
|
15
15
|
|
|
16
16
|
Can be considered as `ApolloClient` for protocols other than `GraphQL`, but with **full control** over its storage - redux store, with ability to write custom selectors, actions and reducers to manage cached state.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
Examples of states, generated by cache reducer from `/example` project:
|
|
19
|
+
<details>
|
|
20
|
+
<summary>
|
|
21
|
+
Normalized
|
|
22
|
+
</summary>
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
{
|
|
26
|
+
entities: {
|
|
27
|
+
// each typename has its own map of entities, stored by id
|
|
28
|
+
users: {
|
|
29
|
+
"0": {id: 0, bankId: "0", name: "User 0 *"},
|
|
30
|
+
"1": {id: 1, bankId: "1", name: "User 1 *"},
|
|
31
|
+
"2": {id: 2, bankId: "2", name: "User 2"},
|
|
32
|
+
"3": {id: 3, bankId: "3", name: "User 3"}
|
|
33
|
+
},
|
|
34
|
+
banks: {
|
|
35
|
+
"0": {id: "0", name: "Bank 0"},
|
|
36
|
+
"1": {id: "1", name: "Bank 1"},
|
|
37
|
+
"2": {id: "2", name: "Bank 2"},
|
|
38
|
+
"3": {id: "3", name: "Bank 3"}
|
|
39
|
+
}
|
|
28
40
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
queries: {
|
|
42
|
+
// each query has its own map of query states, stored by cache key, which is generated from query params
|
|
43
|
+
getUser: {
|
|
44
|
+
"2": {loading: false, error: undefined, result: 2, params: 2},
|
|
45
|
+
"3": {loading: true, params: 3}
|
|
46
|
+
},
|
|
47
|
+
getUsers: {
|
|
48
|
+
// example of paginated state under custom cache key
|
|
49
|
+
"all-pages": {
|
|
50
|
+
loading: false,
|
|
51
|
+
result: {items: [0,1,2], page: 1},
|
|
52
|
+
params: {page: 1}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
},
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
56
|
+
mutations: {
|
|
57
|
+
// each mutation has its own state as well
|
|
58
|
+
updateUser: {
|
|
59
|
+
loading: false,
|
|
60
|
+
result: 1,
|
|
61
|
+
params: {id: 1, name: "User 1 *"}
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
|
-
},
|
|
47
|
-
mutations: {
|
|
48
|
-
// each mutation has its own state as well
|
|
49
|
-
updateUser: {loading: false, result: 1}
|
|
50
64
|
}
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
```
|
|
66
|
+
</details>
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
<details>
|
|
69
|
+
<summary>
|
|
70
|
+
Not normalized
|
|
71
|
+
</summary>
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
{
|
|
75
|
+
// entities map is used for normalization and is empty here
|
|
76
|
+
entities: {},
|
|
77
|
+
queries: {
|
|
78
|
+
// each query has its own map of query states, stored by cache key, which is generated from query params
|
|
79
|
+
getUser: {
|
|
80
|
+
"2": {
|
|
81
|
+
loading: false,
|
|
82
|
+
error: undefined,
|
|
83
|
+
result: {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"},
|
|
84
|
+
params: 2
|
|
85
|
+
},
|
|
86
|
+
"3": {loading: true, params: 3}
|
|
87
|
+
},
|
|
88
|
+
getUsers: {
|
|
89
|
+
// example of paginated state under custom cache key
|
|
90
|
+
"all-pages": {
|
|
91
|
+
loading: false,
|
|
92
|
+
result: {
|
|
93
|
+
items: [
|
|
94
|
+
{id: 0, bank: {id: "0", name: "Bank 0"}, name: "User 0 *"},
|
|
95
|
+
{id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
|
|
96
|
+
{id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"}
|
|
97
|
+
],
|
|
98
|
+
page: 1
|
|
99
|
+
},
|
|
100
|
+
params: {page: 1}
|
|
76
101
|
}
|
|
77
102
|
}
|
|
103
|
+
},
|
|
104
|
+
mutations: {
|
|
105
|
+
// each mutation has its own state as well
|
|
106
|
+
updateUser: {
|
|
107
|
+
loading: false,
|
|
108
|
+
result: {id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
|
|
109
|
+
params: {id: 1, name: "User 1 *"}
|
|
110
|
+
}
|
|
78
111
|
}
|
|
79
|
-
},
|
|
80
|
-
mutations: {
|
|
81
|
-
// each mutation has its own state as well
|
|
82
|
-
updateUser: {loading: false, result: {id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"}}
|
|
83
112
|
}
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
```
|
|
114
|
+
</details>
|
|
86
115
|
|
|
87
116
|
### Table of contents
|
|
88
117
|
|
|
@@ -219,44 +248,49 @@ export const UserScreen = () => {
|
|
|
219
248
|
|
|
220
249
|
### Advanced
|
|
221
250
|
|
|
222
|
-
####
|
|
223
|
-
|
|
224
|
-
By default result of a query is stored under its **cache key**, but sometimes it makes sense to take result from other queries or normalized entities.
|
|
251
|
+
#### Advanced cache policy & skip / fetch usage
|
|
225
252
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
For that case `resultSelector` can be used:
|
|
253
|
+
`cache-first` cache policy 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 `skip` parameter of a query:
|
|
229
254
|
|
|
230
255
|
```typescript
|
|
256
|
+
export const UserScreen = () => {
|
|
257
|
+
const user = useSelectEntityById(userId, 'users')
|
|
231
258
|
|
|
232
|
-
|
|
259
|
+
const [{loading, error}] = useQuery({
|
|
260
|
+
query: 'getUser',
|
|
261
|
+
params: userId,
|
|
262
|
+
skip: !!user // we skip fetching if we already have user cached by some other query, e.g. getUsers
|
|
263
|
+
})
|
|
233
264
|
|
|
234
|
-
... = createCache({
|
|
235
265
|
...
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
We can additional check that entity is full enough:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
skip: !!user && isFullUser(user)
|
|
273
|
+
```
|
|
244
274
|
|
|
245
|
-
|
|
275
|
+
Other approach is to set `skip: true` and manually run `fetch` when needed:
|
|
246
276
|
|
|
277
|
+
```typescript
|
|
247
278
|
export const UserScreen = () => {
|
|
248
|
-
|
|
279
|
+
const screenIsVisible = useScreenIsVisible()
|
|
249
280
|
|
|
250
|
-
|
|
251
|
-
// and cached value is returned if user entity was already in the cache
|
|
252
|
-
const [{result, loading, error}] = useQuery({
|
|
281
|
+
const [{result, loading, error}, fetchUser] = useQuery({
|
|
253
282
|
query: 'getUser',
|
|
254
283
|
params: userId,
|
|
284
|
+
skip: true
|
|
255
285
|
})
|
|
256
286
|
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (screenIsVisible) {
|
|
289
|
+
fetchUser()
|
|
290
|
+
}
|
|
291
|
+
}, [screenIsVisible])
|
|
257
292
|
...
|
|
258
293
|
}
|
|
259
|
-
|
|
260
294
|
```
|
|
261
295
|
|
|
262
296
|
#### Infinite scroll pagination
|
|
@@ -295,11 +329,14 @@ Here is an example of `getUsers` query configuration with pagination support. Yo
|
|
|
295
329
|
export const GetUsersScreen = () => {
|
|
296
330
|
const {query} = useClient()
|
|
297
331
|
|
|
298
|
-
const [{result: usersResult, loading, error}, refetch] = useQuery({
|
|
332
|
+
const [{result: usersResult, loading, error, params}, refetch] = useQuery({
|
|
299
333
|
query: 'getUsers',
|
|
300
334
|
params: 1 // page
|
|
301
335
|
})
|
|
302
336
|
|
|
337
|
+
const refreshing = loading && params === 1
|
|
338
|
+
const loadingNextPage = loading && !refreshing
|
|
339
|
+
|
|
303
340
|
const onLoadNextPage = () => {
|
|
304
341
|
const lastLoadedPage = usersResult?.page ?? 0
|
|
305
342
|
query({
|
|
@@ -316,9 +353,14 @@ export const GetUsersScreen = () => {
|
|
|
316
353
|
|
|
317
354
|
return (
|
|
318
355
|
<div>
|
|
356
|
+
{refreshing && <div className="spinner" />}
|
|
319
357
|
{usersResult?.items.map(renderUser)}
|
|
320
358
|
<button onClick={refetch}>Refresh</button>
|
|
321
|
-
|
|
359
|
+
{loadingNextPage ? (
|
|
360
|
+
<div className="spinner" />
|
|
361
|
+
) : (
|
|
362
|
+
<button onClick={loadNextPage}>Load next page</button>
|
|
363
|
+
)}
|
|
322
364
|
</div>
|
|
323
365
|
)
|
|
324
366
|
}
|
package/dist/index.js
CHANGED
|
@@ -23,9 +23,8 @@ Object.defineProperty(exports, "defaultGetCacheKey", { enumerable: true, get: fu
|
|
|
23
23
|
Object.defineProperty(exports, "defaultQueryMutationState", { enumerable: true, get: function () { return utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE; } });
|
|
24
24
|
// Backlog
|
|
25
25
|
// ! high
|
|
26
|
-
//
|
|
27
|
-
// update readme with how to use mergeResults for invalidating / updating caches
|
|
28
|
-
// try use skip for refreshing strategy?
|
|
26
|
+
// useQuery refresh with params use as client.query?
|
|
27
|
+
// update readme with how to use mergeResults for invalidating / updating caches?
|
|
29
28
|
// optimistic response
|
|
30
29
|
// make query key / cache key difference more clear in the docs, and/or rename queryKey -> query
|
|
31
30
|
// ! medium
|
|
@@ -33,14 +32,12 @@ Object.defineProperty(exports, "defaultQueryMutationState", { enumerable: true,
|
|
|
33
32
|
// type extractors from cache
|
|
34
33
|
// custom useStore
|
|
35
34
|
// return back deserialize selector?
|
|
36
|
-
// resultSelector - return also boolean that result is full enough or make cache policy as a function
|
|
37
35
|
// selector for entities by typename
|
|
38
36
|
// callback option on error / success?
|
|
39
37
|
// refetch queries on mutation success
|
|
40
38
|
// remove query/mutation state when it finished without errors
|
|
41
39
|
// deep equal entities while merging state
|
|
42
40
|
// make error type generic
|
|
43
|
-
// don't cache result if resultSelector set? throw error if mergeResult set with resultSelector?
|
|
44
41
|
// ! low
|
|
45
42
|
// access to currently loading queries and mutations?
|
|
46
43
|
// add params to the state?
|
package/dist/query.js
CHANGED
|
@@ -15,7 +15,6 @@ const query = (logTag, store, cache, { updateQueryStateAndEntities, }, queryKey,
|
|
|
15
15
|
var _a;
|
|
16
16
|
const logsEnabled = cache.options.logsEnabled;
|
|
17
17
|
const cacheStateSelector = cache.cacheStateSelector;
|
|
18
|
-
const cacheResultSelector = cache.queries[queryKey].resultSelector;
|
|
19
18
|
const mergeResults = cache.queries[queryKey].mergeResults;
|
|
20
19
|
const queryStateOnStart = cacheStateSelector(store.getState()).queries[queryKey][cacheKey];
|
|
21
20
|
if (queryStateOnStart === null || queryStateOnStart === void 0 ? void 0 : queryStateOnStart.loading) {
|
|
@@ -49,22 +48,16 @@ const query = (logTag, store, cache, { updateQueryStateAndEntities, }, queryKey,
|
|
|
49
48
|
const newState = {
|
|
50
49
|
error: undefined,
|
|
51
50
|
loading: false,
|
|
52
|
-
result:
|
|
53
|
-
?
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(_a = cacheStateSelector(store.getState()).queries[queryKey][cacheKey]) === null || _a === void 0 ? void 0 : _a.result, response, params, store)
|
|
58
|
-
: response.result,
|
|
51
|
+
result: mergeResults
|
|
52
|
+
? mergeResults(
|
|
53
|
+
// @ts-expect-error fix later
|
|
54
|
+
(_a = cacheStateSelector(store.getState()).queries[queryKey][cacheKey]) === null || _a === void 0 ? void 0 : _a.result, response, params, store)
|
|
55
|
+
: response.result,
|
|
59
56
|
};
|
|
60
57
|
store.dispatch(updateQueryStateAndEntities(queryKey, cacheKey, newState, response));
|
|
61
58
|
return {
|
|
62
59
|
// @ts-expect-error fix types
|
|
63
|
-
result:
|
|
64
|
-
? cacheResultSelector(cacheStateSelector(store.getState()),
|
|
65
|
-
// @ts-expect-error fix types
|
|
66
|
-
params)
|
|
67
|
-
: newState === null || newState === void 0 ? void 0 : newState.result,
|
|
60
|
+
result: newState === null || newState === void 0 ? void 0 : newState.result,
|
|
68
61
|
};
|
|
69
62
|
});
|
|
70
63
|
exports.query = query;
|
package/dist/types.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type Cache<N extends string, T extends Typenames, QP, QR, MP, MR> = {
|
|
|
32
32
|
} */
|
|
33
33
|
typenames: T;
|
|
34
34
|
queries: {
|
|
35
|
-
[QK in keyof (QP & QR)]: QK extends keyof (QP | QR) ? QueryInfo<T, QP[QK], QR[QK]
|
|
35
|
+
[QK in keyof (QP & QR)]: QK extends keyof (QP | QR) ? QueryInfo<T, QP[QK], QR[QK]> : never;
|
|
36
36
|
};
|
|
37
37
|
mutations: {
|
|
38
38
|
[MK in keyof (MP & MR)]: MK extends keyof (MP | MR) ? MutationInfo<T, MP[MK], MR[MK]> : never;
|
|
@@ -63,19 +63,13 @@ export type EntityIds<T extends Typenames> = {
|
|
|
63
63
|
[K in keyof T]?: Key[];
|
|
64
64
|
};
|
|
65
65
|
export type Query<T extends Typenames, P, R> = (params: P) => Promise<QueryResponse<T, R>>;
|
|
66
|
-
export type QueryInfo<T extends Typenames, P, R
|
|
66
|
+
export type QueryInfo<T extends Typenames, P, R> = {
|
|
67
67
|
query: Query<T, P, R>;
|
|
68
68
|
/**
|
|
69
69
|
* Cache policy.
|
|
70
70
|
* @default cache-first
|
|
71
71
|
*/
|
|
72
72
|
cachePolicy?: QueryCachePolicy;
|
|
73
|
-
/**
|
|
74
|
-
* Selector for query result from redux state.
|
|
75
|
-
* Can prevent hook from doing unnecessary fetches.
|
|
76
|
-
* Needed when query result may already be in the cache, e.g. for single entity query by id.
|
|
77
|
-
* */
|
|
78
|
-
resultSelector?: (state: S, params: P) => R | undefined;
|
|
79
73
|
/** Merges results before saving to the store. Default implementation is using the latest result. */
|
|
80
74
|
mergeResults?: (oldResult: R | undefined, response: QueryResponse<T, R>, params: P | undefined, store: Store) => R;
|
|
81
75
|
/**
|
|
@@ -89,7 +83,7 @@ export type UseQueryOptions<T extends Typenames, QP, QR, QK extends keyof (QP &
|
|
|
89
83
|
query: QK;
|
|
90
84
|
params: QK extends keyof (QP | QR) ? QP[QK] : never;
|
|
91
85
|
skip?: boolean;
|
|
92
|
-
} & Pick<QueryInfo<T, unknown, unknown
|
|
86
|
+
} & Pick<QueryInfo<T, unknown, unknown>, 'cachePolicy'>;
|
|
93
87
|
/**
|
|
94
88
|
* @param cache-first for each params key fetch is not called if cache exists.
|
|
95
89
|
* @param cache-and-fetch for each params key result is taken from cache and fetch is called.
|
package/dist/useQuery.js
CHANGED
|
@@ -19,32 +19,18 @@ const useQuery = (cache, actions, options) => {
|
|
|
19
19
|
const { query: queryKey, skip, params, cachePolicy = (_a = cache.queries[queryKey].cachePolicy) !== null && _a !== void 0 ? _a : 'cache-first', } = options;
|
|
20
20
|
const logsEnabled = cache.options.logsEnabled;
|
|
21
21
|
const getCacheKey = (_b = cache.queries[queryKey].getCacheKey) !== null && _b !== void 0 ? _b : (utilsAndConstants_1.defaultGetCacheKey);
|
|
22
|
-
const cacheResultSelector = cache.queries[queryKey].resultSelector;
|
|
23
22
|
const cacheStateSelector = cache.cacheStateSelector;
|
|
24
23
|
const store = (0, react_redux_1.useStore)();
|
|
25
24
|
// @ts-expect-error fix types later
|
|
26
25
|
const cacheKey = getCacheKey(params);
|
|
27
|
-
const resultSelector = (0, react_1.useMemo)(() => {
|
|
28
|
-
return (cacheResultSelector &&
|
|
29
|
-
((state) => cacheResultSelector(cacheStateSelector(state),
|
|
30
|
-
// @ts-expect-error fix types later
|
|
31
|
-
params)));
|
|
32
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
-
}, [cacheKey]);
|
|
34
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
35
|
-
const resultFromSelector = (resultSelector && (0, react_redux_1.useSelector)(resultSelector));
|
|
36
|
-
const hasResultFromSelector = resultFromSelector !== undefined;
|
|
37
26
|
const fetch = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
27
|
return yield (0, query_1.query)('useQuery.fetch', store, cache, actions, queryKey, cacheKey, params);
|
|
39
28
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
29
|
}), [store, queryKey, cacheKey]);
|
|
41
|
-
const
|
|
30
|
+
const queryState = (_c = (0, react_redux_1.useSelector)((state) => {
|
|
42
31
|
const queryState = cacheStateSelector(state).queries[queryKey][cacheKey];
|
|
43
32
|
return queryState; // TODO proper type
|
|
44
33
|
})) !== null && _c !== void 0 ? _c : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
|
|
45
|
-
const queryState = hasResultFromSelector
|
|
46
|
-
? (Object.assign(Object.assign({}, queryStateFromSelector), { result: resultFromSelector }))
|
|
47
|
-
: queryStateFromSelector;
|
|
48
34
|
(0, react_1.useEffect)(() => {
|
|
49
35
|
if (skip) {
|
|
50
36
|
logsEnabled && (0, utilsAndConstants_1.log)('useQuery.useEffect skip fetch', { skip, cacheKey });
|
|
@@ -65,7 +51,6 @@ const useQuery = (cache, actions, options) => {
|
|
|
65
51
|
(0, utilsAndConstants_1.log)('useQuery', {
|
|
66
52
|
cacheKey,
|
|
67
53
|
options,
|
|
68
|
-
resultFromSelector,
|
|
69
54
|
queryState,
|
|
70
55
|
});
|
|
71
56
|
return [
|
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.
|
|
5
|
+
"version": "0.5.0",
|
|
6
6
|
"description": "Powerful data fetching and caching library that supports normalization, built on top of redux",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|