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 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
- Example of **normalized** redux state, generated by cache reducer from `/example` project:
19
- ```js
20
- {
21
- entities: {
22
- // each typename has its own map of entities, stored by id
23
- users: {
24
- "0": {id: 0, bankId: "0", name: "User 0 *"},
25
- "1": {id: 1, bankId: "1", name: "User 1 *"},
26
- "2": {id: 2, bankId: "2", name: "User 2"},
27
- "3": {id: 3, bankId: "3", name: "User 3"}
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
- banks: {
30
- "0": {id: "0", name: "Bank 0"},
31
- "1": {id: "1", name: "Bank 1"},
32
- "2": {id: "2", name: "Bank 2"},
33
- "3": {id: "3", name: "Bank 3"}
34
- }
35
- },
36
- queries: {
37
- // each query has its own map of query states, stored by cache key, which is generated from query params
38
- getUser: {
39
- "2": {loading: false, error: undefined, result: 2},
40
- "3": {loading: true}
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
- getUsers: {
43
- // example of paginated state under custom cache key
44
- "all-pages": {loading: false, result: {items: [0,1,2], page: 1}}
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
- Example of **not normalized** redux state, generated by cache reducer from `/example` project:
55
- ```js
56
- {
57
- // entities map is used for normalization and is empty here
58
- entities: {},
59
- queries: {
60
- // each query has its own map of query states, stored by cache key, which is generated from query params
61
- getUser: {
62
- "2": {loading: false, error: undefined, result: {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"}},
63
- "3": {loading: true}
64
- },
65
- getUsers: {
66
- // example of paginated state under custom cache key
67
- "all-pages": {
68
- loading: false,
69
- result: {
70
- items: [
71
- {id: 0, bank: {id: "0", name: "Bank 0"}, name: "User 0 *"},
72
- {id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
73
- {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"}
74
- ],
75
- page: 1
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
- #### resultSelector
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
- For example when single `User` entity is requested by `userId` for the first time, the entity can already be in the cache after `getUsers` query finished.
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
- // createCache
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
- queries: {
237
- ...
238
- getUser: {
239
- query: getUser,
240
- resultSelector: (state, id) => state.entities.users[id]?.id, // <-- Result is selected from cached entities
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
- // component
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
- // When screen mounts for the first time, query is not fetched
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
- <button onClick={onLoadNextPage}>Load next page</button>
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
- // determine refresh vs load next page
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: cacheResultSelector
53
- ? undefined
54
- : mergeResults
55
- ? mergeResults(
56
- // @ts-expect-error fix later
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: cacheResultSelector
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], ReduxCacheState<T, QP, QR, MP, MR>> : never;
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, S> = {
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, unknown>, 'cachePolicy'>;
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 queryStateFromSelector = (_c = (0, react_redux_1.useSelector)((state) => {
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.4.3",
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",