react-redux-cache 0.5.1 → 0.6.1

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
@@ -7,7 +7,7 @@
7
7
  <b>DOGE:</b> D7GMQdKhKC9ymbT9PtcetSFTQjyPRRfkwT <br>
8
8
  </details>
9
9
 
10
- # react-redux-cache
10
+ # react-redux-cache (RRC)
11
11
 
12
12
  **Powerful** yet **lightweight** data fetching and caching library that supports **normalization** unlike `react-query` and `rtk-query`, while having similar but very simple interface. Built on top of `redux`, covered with tests, fully typed and written on Typescript.
13
13
 
@@ -122,15 +122,17 @@ Examples of states, generated by cache reducer from `/example` project:
122
122
  - [api.ts](https://github.com/gentlee/react-redux-cache#apits)
123
123
  - [Usage](https://github.com/gentlee/react-redux-cache#usage)
124
124
  - [Advanced](https://github.com/gentlee/react-redux-cache#advanced)
125
- - [Advanced cache policy](https://github.com/gentlee/react-redux-cache#advanced-cache-policy)
125
+ - [Extended cache policy](https://github.com/gentlee/react-redux-cache#extended-cache-policy)
126
126
  - [Infinite scroll pagination](https://github.com/gentlee/react-redux-cache#infinite-scroll-pagination)
127
127
  - [redux-persist](https://github.com/gentlee/react-redux-cache#redux-persist)
128
128
  - [FAQ](https://github.com/gentlee/react-redux-cache#faq)
129
129
  - [What is a query cache key?](https://github.com/gentlee/react-redux-cache#what-is-a-query-cache-key)
130
- - [How mutation fetching differs from queries?](https://github.com/gentlee/react-redux-cache#how-mutation-fetching-differs-from-queries)
130
+ - [How race conditions are handled?](https://github.com/gentlee/react-redux-cache#how-race-conditions-are-handled)
131
131
 
132
132
  ### Installation
133
133
  `react`, `redux` and `react-redux` are peer dependencies.
134
+
135
+ `fast-deep-equal` is an optional peer dependency if `deepComparisonEnabled` cache option is enabled (default is true).
134
136
  ```sh
135
137
  npm add react-redux-cache react redux react-redux
136
138
  ```
@@ -146,7 +148,7 @@ export const {
146
148
  } = createCache({
147
149
  // Used as prefix for actions and in default cacheStateSelector for selecting cache state from redux state.
148
150
  name: 'cache',
149
- // Typenames provide a mapping of all typenames to their entity types, which is needed for proper typing and normalization.
151
+ // Mapping of all typenames to their entity types, which is needed for proper normalization. Should be empty if normalization is not needed.
150
152
  // Empty objects with type casting can be used as values.
151
153
  typenames: {
152
154
  users: {} as User, // here `users` entities will have type `User`
@@ -162,6 +164,24 @@ export const {
162
164
  },
163
165
  })
164
166
  ```
167
+
168
+ For normalization two things are required:
169
+ - Set proper typenames while creating the cache - mapping of all entities and their corresponding TS types.
170
+ - Return an object from queries and mutations, that contains the following fields (besides `result`):
171
+
172
+ ```typescript
173
+ type EntityChanges<T extends Typenames> = {
174
+ /** Entities that will be merged with existing. */
175
+ merge?: PartialEntitiesMap<T>
176
+ /** Entities that will replace existing. */
177
+ replace?: Partial<EntitiesMap<T>>
178
+ /** Ids of entities that will be removed. */
179
+ remove?: EntityIds<T>
180
+ /** Alias for `merge` to support normalizr. */
181
+ entities?: EntityChanges<T>['merge']
182
+ }
183
+ ```
184
+
165
185
  #### store.ts
166
186
  ```typescript
167
187
  // Create store as usual, passing the new cache reducer under the name of the cache.
@@ -248,31 +268,33 @@ export const UserScreen = () => {
248
268
 
249
269
  ### Advanced
250
270
 
251
- #### Advanced cache policy
271
+ #### Extended cache policy
252
272
 
253
273
  `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:
254
274
 
255
275
  ```typescript
256
276
  export const UserScreen = () => {
277
+ ...
278
+
257
279
  const user = useSelectEntityById(userId, 'users')
258
280
 
259
281
  const [{loading, error}] = useQuery({
260
282
  query: 'getUser',
261
283
  params: userId,
262
- skip: !!user // we skip fetching if we already have user cached by some other query, e.g. getUsers
284
+ skip: !!user // skip fetching if we already have user cached by some other query, e.g. getUsers
263
285
  })
264
286
 
265
287
  ...
266
288
  }
267
289
  ```
268
290
 
269
- We can additional check that entity is full enough:
291
+ We can additionally check that entity is full or "fresh" enough:
270
292
 
271
293
  ```typescript
272
- skip: !!user && isFullUser(user)
294
+ skip: !!user && isFullUser(user)
273
295
  ```
274
296
 
275
- Other approach is to set `skip: true` and manually run `fetch` when needed:
297
+ Another approach is to set `skip: true` and manually run `fetch` when needed:
276
298
 
277
299
  ```typescript
278
300
  export const UserScreen = () => {
@@ -289,6 +311,7 @@ export const UserScreen = () => {
289
311
  fetchUser()
290
312
  }
291
313
  }, [screenIsVisible])
314
+
292
315
  ...
293
316
  }
294
317
  ```
@@ -412,8 +435,8 @@ It is recommended to override it when default implementation is not optimal or w
412
435
 
413
436
  As example, can be overriden when implementing pagination.
414
437
 
415
- #### How mutation fetching differs from queries?
438
+ #### How race conditions are handled?
416
439
 
417
- **Queries:** For each cache key (= unique params by default) of each query fetch is running in parallel. If fetch is already running for specific cache key, all next fetches are cancelled until it finishes.
440
+ **Queries:** Queries are throttled: query with the same cache key (generated from params by default) is cancelled if already running.
418
441
 
419
- **Mutations:** Only one mutation can be run for each mutation key at a time. If another one called, previous is aborted.
442
+ **Mutations:** Mutations are debounced: previous similar mutation is aborted if it was running when the new one started. Second argument in mutations is `AbortSignal`, which can be used e.g. for cancelling http requests.
@@ -3,22 +3,22 @@ export type ActionMap<N extends string, T extends Typenames, QP, QR, MP, MR> = R
3
3
  export declare const createActions: <N extends string, T extends Typenames, QP, QR, MP, MR>(name: N) => {
4
4
  /** Updates query state, and optionally merges entity changes in a single action. */
5
5
  updateQueryStateAndEntities: {
6
- <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QP[K], QR[K]>> | undefined, entityChagnes?: EntityChanges<T> | undefined): {
6
+ <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QP[K], QR[K]>> | undefined, entityChanges?: EntityChanges<T> | undefined): {
7
7
  type: `@rrc/${N}/updateQueryStateAndEntities`;
8
8
  queryKey: K;
9
9
  queryCacheKey: Key;
10
10
  state: Partial<QueryMutationState<QP[K], QR[K]>> | undefined;
11
- entityChagnes: EntityChanges<T> | undefined;
11
+ entityChanges: EntityChanges<T> | undefined;
12
12
  };
13
13
  type: `@rrc/${N}/updateQueryStateAndEntities`;
14
14
  };
15
15
  /** Updates mutation state, and optionally merges entity changes in a single action. */
16
16
  updateMutationStateAndEntities: {
17
- <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined, entityChagnes?: EntityChanges<T> | undefined): {
17
+ <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined, entityChanges?: EntityChanges<T> | undefined): {
18
18
  type: `@rrc/${N}/updateMutationStateAndEntities`;
19
19
  mutationKey: K_1;
20
20
  state: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined;
21
- entityChagnes: EntityChanges<T> | undefined;
21
+ entityChanges: EntityChanges<T> | undefined;
22
22
  };
23
23
  type: `@rrc/${N}/updateMutationStateAndEntities`;
24
24
  };
@@ -5,20 +5,20 @@ const utilsAndConstants_1 = require("./utilsAndConstants");
5
5
  const createActions = (name) => {
6
6
  const actionPrefix = `@${utilsAndConstants_1.PACKAGE_SHORT_NAME}/${name}/`;
7
7
  const updateQueryStateAndEntitiesType = `${actionPrefix}updateQueryStateAndEntities`;
8
- const updateQueryStateAndEntities = (queryKey, queryCacheKey, state, entityChagnes) => ({
8
+ const updateQueryStateAndEntities = (queryKey, queryCacheKey, state, entityChanges) => ({
9
9
  type: updateQueryStateAndEntitiesType,
10
10
  queryKey,
11
11
  queryCacheKey,
12
12
  state,
13
- entityChagnes,
13
+ entityChanges,
14
14
  });
15
15
  updateQueryStateAndEntities.type = updateQueryStateAndEntitiesType;
16
16
  const updateMutationStateAndEntitiesType = `${actionPrefix}updateMutationStateAndEntities`;
17
- const updateMutationStateAndEntities = (mutationKey, state, entityChagnes) => ({
17
+ const updateMutationStateAndEntities = (mutationKey, state, entityChanges) => ({
18
18
  type: updateMutationStateAndEntitiesType,
19
19
  mutationKey,
20
20
  state,
21
- entityChagnes,
21
+ entityChanges,
22
22
  });
23
23
  updateMutationStateAndEntities.type = updateMutationStateAndEntitiesType;
24
24
  const mergeEntityChangesType = `${actionPrefix}mergeEntityChanges`;
@@ -18,12 +18,12 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
18
18
  queryKey: keyof QP & keyof QR;
19
19
  queryCacheKey: Key;
20
20
  state: Partial<QueryMutationState<QP[keyof QP & keyof QR], QR[keyof QP & keyof QR]>> | undefined;
21
- entityChagnes: import("./types").EntityChanges<T> | undefined;
21
+ entityChanges: import("./types").EntityChanges<T> | undefined;
22
22
  } | {
23
23
  type: `@rrc/${N}/updateMutationStateAndEntities`;
24
24
  mutationKey: keyof MP & keyof MR;
25
25
  state: Partial<QueryMutationState<MP[keyof MP & keyof MR], MR[keyof MP & keyof MR]>> | undefined;
26
- entityChagnes: import("./types").EntityChanges<T> | undefined;
26
+ entityChanges: import("./types").EntityChanges<T> | undefined;
27
27
  } | {
28
28
  type: `@rrc/${N}/mergeEntityChanges`;
29
29
  changes: import("./types").EntityChanges<T>;
@@ -43,21 +43,21 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
43
43
  };
44
44
  actions: {
45
45
  updateQueryStateAndEntities: {
46
- <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QP[K], QR[K]>> | undefined, entityChagnes?: import("./types").EntityChanges<T> | undefined): {
46
+ <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QP[K], QR[K]>> | undefined, entityChanges?: import("./types").EntityChanges<T> | undefined): {
47
47
  type: `@rrc/${N}/updateQueryStateAndEntities`;
48
48
  queryKey: K;
49
49
  queryCacheKey: Key;
50
50
  state: Partial<QueryMutationState<QP[K], QR[K]>> | undefined;
51
- entityChagnes: import("./types").EntityChanges<T> | undefined;
51
+ entityChanges: import("./types").EntityChanges<T> | undefined;
52
52
  };
53
53
  type: `@rrc/${N}/updateQueryStateAndEntities`;
54
54
  };
55
55
  updateMutationStateAndEntities: {
56
- <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined, entityChagnes?: import("./types").EntityChanges<T> | undefined): {
56
+ <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined, entityChanges?: import("./types").EntityChanges<T> | undefined): {
57
57
  type: `@rrc/${N}/updateMutationStateAndEntities`;
58
58
  mutationKey: K_1;
59
59
  state: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined;
60
- entityChagnes: import("./types").EntityChanges<T> | undefined;
60
+ entityChanges: import("./types").EntityChanges<T> | undefined;
61
61
  };
62
62
  type: `@rrc/${N}/updateMutationStateAndEntities`;
63
63
  };
@@ -91,11 +91,11 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
91
91
  };
92
92
  selectors: {
93
93
  /** Selects query state. */
94
- selectQueryState: <QK_1 extends keyof QP | keyof QR>(state: unknown, query: QK_1, cacheKey: Key) => QueryMutationState<QK_1 extends keyof QP & keyof QR ? QP[QK_1] : never, QK_1 extends keyof QP & keyof QR ? QR[QK_1] : never> | undefined;
94
+ selectQueryState: <QK_1 extends keyof QP | keyof QR>(state: unknown, query: QK_1, cacheKey: Key) => QueryMutationState<QK_1 extends keyof QP & keyof QR ? QP[QK_1] : never, QK_1 extends keyof QP & keyof QR ? QR[QK_1] : never>;
95
95
  /** Selects query latest result. */
96
96
  selectQueryResult: <QK_2 extends keyof QP | keyof QR>(state: unknown, query: QK_2, cacheKey: Key) => (QK_2 extends keyof QP & keyof QR ? QR[QK_2] : never) | undefined;
97
97
  /** Selects query loading state. */
98
- selectQueryLoading: <QK_3 extends keyof QP | keyof QR>(state: unknown, query: QK_3, cacheKey: Key) => boolean | undefined;
98
+ selectQueryLoading: <QK_3 extends keyof QP | keyof QR>(state: unknown, query: QK_3, cacheKey: Key) => boolean;
99
99
  /** Selects query latest error. */
100
100
  selectQueryError: <QK_4 extends keyof QP | keyof QR>(state: unknown, query: QK_4, cacheKey: Key) => Error | undefined;
101
101
  /** Selects query latest params. */
@@ -109,7 +109,7 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
109
109
  /** Selects mutation latest error. */
110
110
  selectMutationError: <MK_4 extends keyof MP | keyof MR>(state: unknown, mutation: MK_4) => Error | undefined;
111
111
  /** Selects mutation latest params. */
112
- selectMutationParams: <MK_5 extends keyof MP | keyof MR>(state: unknown, mutation: MK_5) => MK_5 extends keyof MP & keyof MR ? MP[MK_5] : never;
112
+ selectMutationParams: <MK_5 extends keyof MP | keyof MR>(state: unknown, mutation: MK_5) => (MK_5 extends keyof MP & keyof MR ? MP[MK_5] : never) | undefined;
113
113
  /** Selects entity by id and typename. */
114
114
  selectEntityById: <TN extends keyof T>(state: unknown, id: Key | null | undefined, typename: TN) => T[TN] | undefined;
115
115
  /** Selects all entities. */
@@ -14,17 +14,18 @@ const utilsAndConstants_1 = require("./utilsAndConstants");
14
14
  * Creates reducer, actions and hooks for managing queries and mutations through redux cache.
15
15
  */
16
16
  const createCache = (partialCache) => {
17
- var _a, _b, _c, _d, _e, _f;
18
- var _g, _h;
17
+ var _a, _b, _c, _d, _e, _f, _g;
18
+ var _h, _j, _k;
19
19
  const abortControllers = new WeakMap();
20
20
  // provide all optional fields
21
21
  (_a = partialCache.options) !== null && _a !== void 0 ? _a : (partialCache.options = {});
22
- (_b = (_g = partialCache.options).logsEnabled) !== null && _b !== void 0 ? _b : (_g.logsEnabled = false);
23
- (_c = (_h = partialCache.options).validateFunctionArguments) !== null && _c !== void 0 ? _c : (_h.validateFunctionArguments = utilsAndConstants_1.IS_DEV);
24
- (_d = partialCache.queries) !== null && _d !== void 0 ? _d : (partialCache.queries = {});
25
- (_e = partialCache.mutations) !== null && _e !== void 0 ? _e : (partialCache.mutations = {});
22
+ (_b = (_h = partialCache.options).logsEnabled) !== null && _b !== void 0 ? _b : (_h.logsEnabled = false);
23
+ (_c = (_j = partialCache.options).validateFunctionArguments) !== null && _c !== void 0 ? _c : (_j.validateFunctionArguments = utilsAndConstants_1.IS_DEV);
24
+ (_d = (_k = partialCache.options).deepComparisonEnabled) !== null && _d !== void 0 ? _d : (_k.deepComparisonEnabled = true);
25
+ (_e = partialCache.queries) !== null && _e !== void 0 ? _e : (partialCache.queries = {});
26
+ (_f = partialCache.mutations) !== null && _f !== void 0 ? _f : (partialCache.mutations = {});
26
27
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
- (_f = partialCache.cacheStateSelector) !== null && _f !== void 0 ? _f : (partialCache.cacheStateSelector = (state) => state[cache.name]);
28
+ (_g = partialCache.cacheStateSelector) !== null && _g !== void 0 ? _g : (partialCache.cacheStateSelector = (state) => state[cache.name]);
28
29
  // @ts-expect-error private field for testing
29
30
  partialCache.abortControllers = abortControllers;
30
31
  const cache = partialCache;
@@ -33,12 +34,14 @@ const createCache = (partialCache) => {
33
34
  return id == null ? undefined : cache.cacheStateSelector(state).entities[typename][id];
34
35
  };
35
36
  const selectQueryState = (state, query, cacheKey) => {
37
+ var _a;
36
38
  // @ts-expect-error fix later
37
- return cache.cacheStateSelector(state).queries[query][cacheKey];
39
+ return (_a = cache.cacheStateSelector(state).queries[query][cacheKey]) !== null && _a !== void 0 ? _a : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
38
40
  };
39
41
  const selectMutationState = (state, mutation) => {
42
+ var _a;
40
43
  // @ts-expect-error fix later
41
- return cache.cacheStateSelector(state).mutations[mutation];
44
+ return (_a = cache.cacheStateSelector(state).mutations[mutation]) !== null && _a !== void 0 ? _a : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
42
45
  };
43
46
  const actions = (0, createActions_1.createActions)(cache.name);
44
47
  return {
@@ -52,23 +55,19 @@ const createCache = (partialCache) => {
52
55
  selectQueryState,
53
56
  /** Selects query latest result. */
54
57
  selectQueryResult: (state, query, cacheKey) => {
55
- var _a;
56
- return (_a = selectQueryState(state, query, cacheKey)) === null || _a === void 0 ? void 0 : _a.result;
58
+ return selectQueryState(state, query, cacheKey).result;
57
59
  },
58
60
  /** Selects query loading state. */
59
61
  selectQueryLoading: (state, query, cacheKey) => {
60
- var _a;
61
- return (_a = selectQueryState(state, query, cacheKey)) === null || _a === void 0 ? void 0 : _a.loading;
62
+ return selectQueryState(state, query, cacheKey).loading;
62
63
  },
63
64
  /** Selects query latest error. */
64
65
  selectQueryError: (state, query, cacheKey) => {
65
- var _a;
66
- return (_a = selectQueryState(state, query, cacheKey)) === null || _a === void 0 ? void 0 : _a.error;
66
+ return selectQueryState(state, query, cacheKey).error;
67
67
  },
68
68
  /** Selects query latest params. */
69
69
  selectQueryParams: (state, query, cacheKey) => {
70
- var _a;
71
- return (_a = selectQueryState(state, query, cacheKey)) === null || _a === void 0 ? void 0 : _a.params;
70
+ return selectQueryState(state, query, cacheKey).params;
72
71
  },
73
72
  /** Selects mutation state. */
74
73
  selectMutationState,
package/dist/index.js CHANGED
@@ -23,11 +23,14 @@ 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
+ // optimistic response
27
+ // type extractors?
28
+ // skip -> enabled?
26
29
  // add full api docs
27
30
  // update readme with how to use mergeResults for invalidating / updating caches?
28
- // optimistic response
29
- // make query key / cache key difference more clear in the docs, and/or rename queryKey -> query
31
+ // make query key / cache key difference more clear in the docs, and/or rename queryKey -> query?
30
32
  // ! medium
33
+ // check type of function arguments in dev
31
34
  // allow multiple mutation with same keys?
32
35
  // type extractors from cache
33
36
  // custom useStore
package/dist/reducer.d.ts CHANGED
@@ -10,12 +10,12 @@ export declare const createCacheReducer: <N extends string, T extends Typenames,
10
10
  queryKey: keyof QP & keyof QR;
11
11
  queryCacheKey: import("./types").Key;
12
12
  state: Partial<QueryMutationState<QP[keyof QP & keyof QR], QR[keyof QP & keyof QR]>> | undefined;
13
- entityChagnes: import("./types").EntityChanges<T> | undefined;
13
+ entityChanges: import("./types").EntityChanges<T> | undefined;
14
14
  } | {
15
15
  type: `@rrc/${N}/updateMutationStateAndEntities`;
16
16
  mutationKey: keyof MP & keyof MR;
17
17
  state: Partial<QueryMutationState<MP[keyof MP & keyof MR], MR[keyof MP & keyof MR]>> | undefined;
18
- entityChagnes: import("./types").EntityChanges<T> | undefined;
18
+ entityChanges: import("./types").EntityChanges<T> | undefined;
19
19
  } | {
20
20
  type: `@rrc/${N}/mergeEntityChanges`;
21
21
  changes: import("./types").EntityChanges<T>;
package/dist/reducer.js CHANGED
@@ -24,24 +24,47 @@ const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
24
24
  queryKeys,
25
25
  initialState,
26
26
  });
27
+ const deepEqual = cacheOptions.deepComparisonEnabled ? utilsAndConstants_1.optionalUtils.deepEqual : undefined;
27
28
  return (state = initialState, action) => {
28
29
  var _a, _b;
29
30
  switch (action.type) {
30
31
  case actions.updateQueryStateAndEntities.type: {
31
- const { queryKey, queryCacheKey, state: queryState, entityChagnes, } = action;
32
- const newEntities = entityChagnes && (0, utilsAndConstants_1.applyEntityChanges)(state.entities, entityChagnes, cacheOptions);
33
- if (!queryState && !newEntities) {
34
- return state;
32
+ const { queryKey, queryCacheKey, state: queryState, entityChanges, } = action;
33
+ const oldQueryState = (_a = state.queries[queryKey][queryCacheKey]) !== null && _a !== void 0 ? _a : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
34
+ let newQueryState = queryState && Object.assign(Object.assign({}, oldQueryState), queryState);
35
+ if (deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldQueryState, newQueryState)) {
36
+ newQueryState = undefined;
37
+ }
38
+ const newEntities = entityChanges && (0, utilsAndConstants_1.applyEntityChanges)(state.entities, entityChanges, cacheOptions);
39
+ let newState;
40
+ if (newEntities) {
41
+ newState !== null && newState !== void 0 ? newState : (newState = Object.assign({}, state));
42
+ newState.entities = newEntities;
43
+ }
44
+ if (newQueryState) {
45
+ newState !== null && newState !== void 0 ? newState : (newState = Object.assign({}, state));
46
+ newState.queries = Object.assign(Object.assign({}, state.queries), { [queryKey]: Object.assign(Object.assign({}, state.queries[queryKey]), { [queryCacheKey]: newQueryState }) });
35
47
  }
36
- return Object.assign(Object.assign(Object.assign({}, state), (newEntities ? { entities: newEntities } : null)), { queries: Object.assign(Object.assign({}, state.queries), { [queryKey]: Object.assign(Object.assign({}, state.queries[queryKey]), { [queryCacheKey]: Object.assign(Object.assign({}, ((_a = state.queries[queryKey][queryCacheKey]) !== null && _a !== void 0 ? _a : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE)), queryState) }) }) });
48
+ return newState !== null && newState !== void 0 ? newState : state;
37
49
  }
38
50
  case actions.updateMutationStateAndEntities.type: {
39
- const { mutationKey, state: mutationState, entityChagnes, } = action;
40
- const newEntities = entityChagnes && (0, utilsAndConstants_1.applyEntityChanges)(state.entities, entityChagnes, cacheOptions);
41
- if (!mutationState && !newEntities) {
42
- return state;
51
+ const { mutationKey, state: mutationState, entityChanges, } = action;
52
+ const oldMutationState = (_b = state.mutations[mutationKey]) !== null && _b !== void 0 ? _b : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
53
+ let newMutationState = mutationState && Object.assign(Object.assign({}, oldMutationState), mutationState);
54
+ if (deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldMutationState, newMutationState)) {
55
+ newMutationState = undefined;
56
+ }
57
+ const newEntities = entityChanges && (0, utilsAndConstants_1.applyEntityChanges)(state.entities, entityChanges, cacheOptions);
58
+ let newState;
59
+ if (newEntities) {
60
+ newState !== null && newState !== void 0 ? newState : (newState = Object.assign({}, state));
61
+ newState.entities = newEntities;
43
62
  }
44
- return Object.assign(Object.assign(Object.assign({}, state), (newEntities ? { entities: newEntities } : null)), { mutations: Object.assign(Object.assign({}, state.mutations), { [mutationKey]: Object.assign(Object.assign({}, ((_b = state.mutations[mutationKey]) !== null && _b !== void 0 ? _b : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE)), mutationState) }) });
63
+ if (newMutationState) {
64
+ newState !== null && newState !== void 0 ? newState : (newState = Object.assign({}, state));
65
+ newState.mutations = Object.assign(Object.assign({}, state.mutations), { [mutationKey]: newMutationState });
66
+ }
67
+ return newState !== null && newState !== void 0 ? newState : state;
45
68
  }
46
69
  case actions.mergeEntityChanges.type: {
47
70
  const { changes } = action;
@@ -55,22 +78,24 @@ const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
55
78
  }
56
79
  let newQueries = undefined;
57
80
  for (const query of queryKeysToClear) {
81
+ const queryState = (newQueries !== null && newQueries !== void 0 ? newQueries : state.queries)[query.key];
58
82
  if (query.cacheKey != null) {
59
- if ((newQueries !== null && newQueries !== void 0 ? newQueries : state.queries)[query.key][query.cacheKey]) {
83
+ if (queryState[query.cacheKey]) {
60
84
  newQueries !== null && newQueries !== void 0 ? newQueries : (newQueries = Object.assign({}, state.queries));
61
- newQueries[query.key] = Object.assign({}, newQueries[query.key]);
85
+ if (state.queries[query.key] === newQueries[query.key]) {
86
+ newQueries[query.key] = Object.assign({}, newQueries[query.key]);
87
+ }
62
88
  delete newQueries[query.key][query.cacheKey];
63
89
  }
64
90
  }
65
- else if ((newQueries !== null && newQueries !== void 0 ? newQueries : state.queries)[query.key] !== EMPTY_QUERY_STATE) {
91
+ else if (queryState !== EMPTY_QUERY_STATE) {
66
92
  newQueries !== null && newQueries !== void 0 ? newQueries : (newQueries = Object.assign({}, state.queries));
67
93
  newQueries[query.key] = EMPTY_QUERY_STATE;
68
94
  }
69
95
  }
70
- if (!newQueries) {
71
- return state;
72
- }
73
- return Object.assign(Object.assign({}, state), { queries: newQueries });
96
+ return !newQueries
97
+ ? state
98
+ : Object.assign(Object.assign({}, state), { queries: newQueries });
74
99
  }
75
100
  case actions.clearMutationState.type: {
76
101
  const { mutationKeys } = action;
@@ -84,10 +109,9 @@ const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
84
109
  delete newMutations[mutation];
85
110
  }
86
111
  }
87
- if (!newMutations) {
88
- return state;
89
- }
90
- return Object.assign(Object.assign({}, state), { mutations: newMutations });
112
+ return !newMutations
113
+ ? state
114
+ : Object.assign(Object.assign({}, state), { mutations: newMutations });
91
115
  }
92
116
  }
93
117
  return state;
package/dist/types.d.ts CHANGED
@@ -22,7 +22,7 @@ export type Cache<N extends string, T extends Typenames, QP, QR, MP, MR> = {
22
22
  /** Used as prefix for actions and in default cacheStateSelector for selecting cache state from redux state. */
23
23
  name: N;
24
24
  /**
25
- * Mapping of all typenames to their entity types, which is needed for proper normalization. Can be empty If normalization not needed.
25
+ * Mapping of all typenames to their entity types, which is needed for proper normalization. Should be empty if normalization is not needed.
26
26
  * @key Typename.
27
27
  * @value Object with proper type of the typename. Empty objects with type casting can be used.
28
28
  * @example
@@ -44,14 +44,21 @@ export type Cache<N extends string, T extends Typenames, QP, QR, MP, MR> = {
44
44
  export type CacheOptions = {
45
45
  /**
46
46
  * Enables validation of package function arguments. Recommened to enable in dev/testing mode.
47
- * Default is true in dev mode.
47
+ * @default true in dev mode.
48
48
  * */
49
49
  validateFunctionArguments: boolean;
50
50
  /**
51
- * Enable console logs.
52
- * Default is false.
51
+ * Enables console logs.
52
+ * @default false
53
53
  */
54
54
  logsEnabled: boolean;
55
+ /**
56
+ * Enables deep comparison before merging entities to the state.
57
+ * Re-rendering is a heavier operation than comparison, so disabling it can lead to performance drop.
58
+ * Makes sense to disable only if merging equal results & entities to the state is a rare case.
59
+ * @default true
60
+ */
61
+ deepComparisonEnabled: boolean;
55
62
  };
56
63
  export type PartialEntitiesMap<T extends Typenames> = {
57
64
  [K in keyof T]?: Dict<Partial<T[K]>>;
@@ -122,5 +129,5 @@ export type QueryMutationState<P, R> = {
122
129
  /** Error of the latest response. */
123
130
  error?: Error;
124
131
  /** Parameters of the latest request. */
125
- params: P;
132
+ params?: P;
126
133
  };
package/dist/useQuery.js CHANGED
@@ -23,6 +23,7 @@ const useQuery = (cache, actions, options) => {
23
23
  const store = (0, react_redux_1.useStore)();
24
24
  // @ts-expect-error fix types later
25
25
  const cacheKey = getCacheKey(params);
26
+ /** Fetch query with the new parameters, or refetch with the same if parameters not provided. */
26
27
  const fetch = (0, react_1.useCallback)((options) => __awaiter(void 0, void 0, void 0, function* () {
27
28
  return yield (0, query_1.query)('useQuery.fetch', store, cache, actions, queryKey,
28
29
  // @ts-expect-error fix later
@@ -30,6 +31,7 @@ const useQuery = (cache, actions, options) => {
30
31
  }),
31
32
  // eslint-disable-next-line react-hooks/exhaustive-deps
32
33
  [store, queryKey, cacheKey]);
34
+ /** Query state */
33
35
  const queryState = (_c = (0, react_redux_1.useSelector)((state) => {
34
36
  const queryState = cacheStateSelector(state).queries[queryKey][cacheKey];
35
37
  return queryState; // TODO proper type
@@ -56,11 +58,6 @@ const useQuery = (cache, actions, options) => {
56
58
  options,
57
59
  queryState,
58
60
  });
59
- return [
60
- /** Query state */
61
- queryState,
62
- /** Refetch query with the same parameters */
63
- fetch,
64
- ];
61
+ return [queryState, fetch];
65
62
  };
66
63
  exports.useQuery = useQuery;
@@ -1,9 +1,11 @@
1
1
  import type { CacheOptions, EntitiesMap, EntityChanges, Key, Typenames } from './types';
2
2
  export declare const PACKAGE_SHORT_NAME = "rrc";
3
+ export declare const optionalUtils: {
4
+ deepEqual?: (a: any, b: any) => boolean;
5
+ };
3
6
  export declare const IS_DEV: boolean;
4
7
  export declare const DEFAULT_QUERY_MUTATION_STATE: {
5
8
  readonly loading: false;
6
- readonly error: undefined;
7
9
  };
8
10
  export declare const defaultGetCacheKey: <P = unknown>(params: P) => Key;
9
11
  export declare const log: (tag: string, data?: unknown) => void;
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyEntityChanges = exports.log = exports.defaultGetCacheKey = exports.DEFAULT_QUERY_MUTATION_STATE = exports.IS_DEV = exports.PACKAGE_SHORT_NAME = void 0;
3
+ exports.applyEntityChanges = exports.log = exports.defaultGetCacheKey = exports.DEFAULT_QUERY_MUTATION_STATE = exports.IS_DEV = exports.optionalUtils = exports.PACKAGE_SHORT_NAME = void 0;
4
4
  exports.PACKAGE_SHORT_NAME = 'rrc';
5
+ exports.optionalUtils = {
6
+ deepEqual: undefined,
7
+ };
8
+ try {
9
+ exports.optionalUtils.deepEqual = require('fast-deep-equal/es6');
10
+ }
11
+ catch (_a) {
12
+ console.debug(exports.PACKAGE_SHORT_NAME + ': fast-deep-equal optional dependency was not installed');
13
+ }
5
14
  exports.IS_DEV = (() => {
6
15
  try {
7
16
  // @ts-expect-error __DEV__ is only for React Native
@@ -11,7 +20,7 @@ exports.IS_DEV = (() => {
11
20
  return process.env.NODE_ENV === 'development';
12
21
  }
13
22
  })();
14
- exports.DEFAULT_QUERY_MUTATION_STATE = { loading: false, error: undefined };
23
+ exports.DEFAULT_QUERY_MUTATION_STATE = { loading: false };
15
24
  const defaultGetCacheKey = (params) => {
16
25
  switch (typeof params) {
17
26
  case 'string':
@@ -40,6 +49,7 @@ const applyEntityChanges = (entities, changes, options) => {
40
49
  if (!merge && !replace && !remove) {
41
50
  return undefined;
42
51
  }
52
+ const deepEqual = options.deepComparisonEnabled ? exports.optionalUtils.deepEqual : undefined;
43
53
  let result;
44
54
  for (const typename in entities) {
45
55
  const entitiesToMerge = merge === null || merge === void 0 ? void 0 : merge[typename];
@@ -60,21 +70,39 @@ const applyEntityChanges = (entities, changes, options) => {
60
70
  throw new Error('Merge, replace and remove changes have intersections for: ' + typename);
61
71
  }
62
72
  }
63
- const newEntities = Object.assign({}, entities[typename]);
73
+ const oldEntities = entities[typename];
74
+ let newEntities;
64
75
  // remove
65
- entitiesToRemove === null || entitiesToRemove === void 0 ? void 0 : entitiesToRemove.forEach((id) => delete newEntities[id]);
76
+ entitiesToRemove === null || entitiesToRemove === void 0 ? void 0 : entitiesToRemove.forEach((id) => {
77
+ if (oldEntities[id]) {
78
+ newEntities !== null && newEntities !== void 0 ? newEntities : (newEntities = Object.assign({}, oldEntities));
79
+ delete newEntities[id];
80
+ }
81
+ });
66
82
  // replace
67
83
  if (entitiesToReplace) {
68
84
  for (const id in entitiesToReplace) {
69
- newEntities[id] = entitiesToReplace[id];
85
+ const newEntity = entitiesToReplace[id];
86
+ if (!(deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldEntities[id], newEntity))) {
87
+ newEntities !== null && newEntities !== void 0 ? newEntities : (newEntities = Object.assign({}, oldEntities));
88
+ newEntities[id] = newEntity;
89
+ }
70
90
  }
71
91
  }
72
92
  // merge
73
93
  if (entitiesToMerge) {
74
94
  for (const id in entitiesToMerge) {
75
- newEntities[id] = Object.assign(Object.assign({}, newEntities[id]), entitiesToMerge[id]);
95
+ const oldEntity = oldEntities[id];
96
+ const newEntity = Object.assign(Object.assign({}, oldEntity), entitiesToMerge[id]);
97
+ if (!(deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldEntity, newEntity))) {
98
+ newEntities !== null && newEntities !== void 0 ? newEntities : (newEntities = Object.assign({}, oldEntities));
99
+ newEntities[id] = newEntity;
100
+ }
76
101
  }
77
102
  }
103
+ if (!newEntities) {
104
+ continue;
105
+ }
78
106
  result !== null && result !== void 0 ? result : (result = Object.assign({}, entities));
79
107
  result[typename] = newEntities;
80
108
  }
@@ -82,6 +110,7 @@ const applyEntityChanges = (entities, changes, options) => {
82
110
  (0, exports.log)('applyEntityChanges', {
83
111
  entities,
84
112
  changes,
113
+ options,
85
114
  result,
86
115
  });
87
116
  return result;
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.1",
5
+ "version": "0.6.1",
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",
@@ -14,6 +14,17 @@
14
14
  "test": "node node_modules/jest/bin/jest.js",
15
15
  "prepublishOnly": "yarn build && yarn test"
16
16
  },
17
+ "peerDependencies": {
18
+ "fast-deep-equal": "*",
19
+ "react": "^16",
20
+ "react-redux": "^4",
21
+ "redux": "^4"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "fast-deep-equal": {
25
+ "optional": true
26
+ }
27
+ },
17
28
  "devDependencies": {
18
29
  "@testing-library/jest-dom": "6.1.4",
19
30
  "@testing-library/react": "14.0.0",
@@ -40,11 +51,6 @@
40
51
  "ts-jest": "29.1.0",
41
52
  "typescript": "5.0"
42
53
  },
43
- "peerDependencies": {
44
- "react": "^16",
45
- "react-redux": "^4",
46
- "redux": "^4"
47
- },
48
54
  "files": [
49
55
  "dist/*"
50
56
  ],