react-redux-cache 0.0.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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Alexander Danilov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # react-redux-cache
2
+
3
+ **Powerfull** and **customizable** data fetching and caching library that supports **normalization** (unlike `react-query` and `rtk-query`), built on top of `redux`.
4
+
5
+ **Normalization** is the only way to keep the state of the app **consistent** between different views, reduces the number of fetches and allows to show cached data when navigating, which greatly improves **user experience**.
6
+
7
+ Hooks, reducer, actions and selectors are fully typed and written on Typescript, so redux store will be properly typed and you will remain a **full control** of its state with ability to write custom selectors, actions and reducers to manage cached state.
8
+
9
+ Usage example can be found in `example/` folder and run by `npm run example` command from the root folder.
10
+
11
+ ### Table of contents
12
+
13
+ - [Installation](https://github.com/gentlee/react-redux-cache#Installation)
14
+ - [Initialization](https://github.com/gentlee/react-redux-cache#Initialization)
15
+ - [cache.ts](https://github.com/gentlee/react-redux-cache#cachets)
16
+ - [store.ts](https://github.com/gentlee/react-redux-cache#storets)
17
+ - [api.ts](https://github.com/gentlee/react-redux-cache#apits)
18
+ - [Usage](https://github.com/gentlee/react-redux-cache#usage)
19
+ - [Advanced](https://github.com/gentlee/react-redux-cache#advanced)
20
+
21
+ ### Installation
22
+ `react` and `redux` are peer dependencies.
23
+ ```sh
24
+ npm add react-redux-cache react redux
25
+ ```
26
+ ### Initialization
27
+ Create reducer, hooks, actions and selectors with `createCache`.
28
+ All queries and mutations should be passed while initializing the cache, for proper typing and later access by key.
29
+ In this example we omit usage of actions and selectors.
30
+ #### cache.ts
31
+ ```typescript
32
+ export const {
33
+ reducer,
34
+ hooks: {useMutation, useQuery, useSelectEntityById},
35
+ } = createCache({
36
+ // This selector should select cache state from redux store state, based on the path to its reducer.
37
+ cacheStateSelector: (state) => state.cache,
38
+ // Typenames provide a mapping of all typenames to their entity types.
39
+ // Empty objects with type casting can be used as values.
40
+ typenames: {
41
+ users: {} as User,
42
+ banks: {} as Bank,
43
+ },
44
+ queries: {
45
+ getUsers: { query: getUsers },
46
+ getUser: { query: getUser },
47
+ },
48
+ mutations: {
49
+ updateUser: { mutation: updateUser },
50
+ removeUser: { mutation: removeUser },
51
+ },
52
+ })
53
+ ```
54
+ #### store.ts
55
+ ```typescript
56
+ const store = configureStore({
57
+ reducer: {
58
+ cache: reducer,
59
+ }
60
+ })
61
+ ```
62
+ #### api.ts
63
+ Query result should be of type `QueryResponse`, mutation result should be of type `MutationResponse`.
64
+ For normalization `normalizr` package is used in this example, but any other tool can be used if query result is of proper type.
65
+ Perfect implementation is when the backend already returns normalized data.
66
+ ```typescript
67
+ export const getUser = async (id: number) => {
68
+ const result: User = await ...
69
+
70
+ const normalizedResult: {
71
+ result: number
72
+ entities: {
73
+ users: Record<number, User>
74
+ banks: Record<string, Bank>
75
+ }
76
+ } = normalize(result, getUserSchema)
77
+
78
+ return normalizedResult
79
+ }
80
+
81
+ export const removeUser = async (id: number) => {
82
+ await ...
83
+ return {
84
+ remove: { users: [id] },
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Usage
90
+ #### UserScreen.tsx
91
+ ```typescript
92
+ export const UserScreen = () => {
93
+ const {id} = useParams()
94
+
95
+ // useQuery will infer all types from created cache,
96
+ // telling you that params and result here are of type `number`.
97
+ const [{result: userId, loading, error}] = useQuery({
98
+ query: 'getUser',
99
+ params: Number(id),
100
+ })
101
+
102
+ const [updateUser, {loading: updatingUser}] = useMutation({
103
+ mutation: 'updateUser',
104
+ })
105
+
106
+ // This selector is created by createCache and also returns proper types - User and Bank
107
+ const user = useSelectEntityById(userId, 'users')
108
+ const bank = useSelectEntityById(user?.bank, 'banks')
109
+
110
+ if (loading) {
111
+ return ... // loading state
112
+ }
113
+
114
+ return ... // loaded state
115
+ }
116
+ ```
117
+
118
+ ### Advanced
119
+ To be done...
@@ -0,0 +1,74 @@
1
+ import { mergeEntityChanges, setMutationStateAndEntities, setQueryStateAndEntities } from './reducer';
2
+ import { Cache, EntitiesMap, Key, OptionalPartial, Typenames } from './types';
3
+ import { useMutation } from './useMutation';
4
+ import { useQuery } from './useQuery';
5
+ export * from './reducer';
6
+ export * from './types';
7
+ export * from './useMutation';
8
+ export * from './useQuery';
9
+ export * from './utilsAndConstants';
10
+ /** Creates reducer, actions and hooks for managing queries and mutations through redux cache. */
11
+ export declare const createCache: <T extends Typenames, QP, QR, MP, MR>(cache: OptionalPartial<Cache<T, QP, QR, MP, MR>, "options">) => {
12
+ cache: Cache<T, QP, QR, MP, MR>;
13
+ /** Reducer of the cache, should be added to redux store. */
14
+ reducer: (state: {
15
+ entities: EntitiesMap<T>;
16
+ queries: { [QK in keyof QR]: import("./types").Dict<import("./types").QueryMutationState<QR[QK]>>; };
17
+ mutations: { [MK in keyof MR]: import("./types").QueryMutationState<MR[MK]>; };
18
+ } | undefined, action: {
19
+ type: `${string}MERGE_ENTITY_CHANGES`;
20
+ changes: import("./types").EntityChanges<T>;
21
+ } | {
22
+ type: `${string}SET_QUERY_STATE_AND_ENTITIES`;
23
+ queryKey: keyof QR;
24
+ queryCacheKey: Key;
25
+ state: Partial<import("./types").QueryMutationState<QR[keyof QR]>> | undefined;
26
+ entityChagnes: import("./types").EntityChanges<T> | undefined;
27
+ } | {
28
+ type: `${string}SET_MUTATION_STATE_AND_ENTITIES`;
29
+ mutationKey: keyof MR;
30
+ state: Partial<import("./types").QueryMutationState<MR[keyof MR]>> | undefined;
31
+ entityChagnes: import("./types").EntityChanges<T> | undefined;
32
+ }) => {
33
+ entities: EntitiesMap<T>;
34
+ queries: { [QK in keyof QR]: import("./types").Dict<import("./types").QueryMutationState<QR[QK]>>; };
35
+ mutations: { [MK in keyof MR]: import("./types").QueryMutationState<MR[MK]>; };
36
+ };
37
+ actions: {
38
+ /** Updates query state, and optionally merges entity changes in a single action. */
39
+ setQueryStateAndEntities: <K extends keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<import("./types").QueryMutationState<QR[K]>> | undefined, entityChagnes?: import("./types").EntityChanges<T> | undefined) => {
40
+ type: `${string}SET_QUERY_STATE_AND_ENTITIES`;
41
+ queryKey: K;
42
+ queryCacheKey: Key;
43
+ state: Partial<import("./types").QueryMutationState<QR[K]>> | undefined;
44
+ entityChagnes: import("./types").EntityChanges<T> | undefined;
45
+ };
46
+ /** Updates mutation state, and optionally merges entity changes in a single action. */
47
+ setMutationStateAndEntities: <K_1 extends keyof MR>(mutationKey: K_1, state?: Partial<import("./types").QueryMutationState<MR[K_1]>> | undefined, entityChagnes?: import("./types").EntityChanges<T> | undefined) => {
48
+ type: `${string}SET_MUTATION_STATE_AND_ENTITIES`;
49
+ mutationKey: K_1;
50
+ state: Partial<import("./types").QueryMutationState<MR[K_1]>> | undefined;
51
+ entityChagnes: import("./types").EntityChanges<T> | undefined;
52
+ };
53
+ /** Merge EntityChanges to the state. */
54
+ mergeEntityChanges: (changes: import("./types").EntityChanges<T>) => {
55
+ type: `${string}MERGE_ENTITY_CHANGES`;
56
+ changes: import("./types").EntityChanges<T>;
57
+ };
58
+ };
59
+ selectors: {
60
+ entitiesSelector: (state: unknown) => EntitiesMap<T>;
61
+ entitiesByTypenameSelector: <TN extends keyof T>(typename: TN) => { [K_2 in keyof T]: (state: unknown) => EntitiesMap<T>[K_2]; }[TN];
62
+ };
63
+ hooks: {
64
+ /** Fetches query when params change and subscribes to query state. */
65
+ useQuery: <QK_1 extends keyof QP | keyof QR>(options: import("./types").UseQueryOptions<T, QP, QR, MP, MR, QK_1>) => readonly [import("./types").QueryMutationState<QK_1 extends keyof QP & keyof QR ? QR[QK_1] : never>, (params?: (QK_1 extends keyof QP & keyof QR ? QP[QK_1] : never) | undefined) => Promise<void>];
66
+ /** Subscribes to provided mutation state and provides mutate function. */
67
+ useMutation: <MK_1 extends keyof MP | keyof MR>(options: {
68
+ mutation: MK_1;
69
+ cacheOptions?: import("./types").MutationCacheOptions | undefined;
70
+ }) => readonly [(params: MK_1 extends keyof MP & keyof MR ? MP[MK_1] : never) => Promise<void>, import("./types").QueryMutationState<MK_1 extends keyof MP & keyof MR ? MP[MK_1] : never>, AbortController | undefined];
71
+ /** Selects entity by id and subscribes to the changes. */
72
+ useSelectEntityById: <K_3 extends keyof T>(id: Key | null | undefined, typename: K_3) => T[K_3] | undefined;
73
+ };
74
+ };
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createCache = void 0;
18
+ const react_redux_1 = require("react-redux");
19
+ const reducer_1 = require("./reducer");
20
+ const useMutation_1 = require("./useMutation");
21
+ const useQuery_1 = require("./useQuery");
22
+ const utilsAndConstants_1 = require("./utilsAndConstants");
23
+ // Backlog
24
+ // ! high
25
+ // create package with README
26
+ // cover with tests
27
+ // ! medium
28
+ // add params to the state
29
+ // selector for entities by typename
30
+ // provide call query/mutation function to call them without hooks, but with all state updates
31
+ // get typenames from schema? (useSelectDenormalized)
32
+ // callback option on error / success?
33
+ // cache policy as function? needsRefetch
34
+ // add verbose debug logs
35
+ // refetch queries on query / mutation success?
36
+ // remove state when it finished without errors
37
+ // set default options like getParams
38
+ // selectors for loading state of similar query or mutation (wihout using params as key)
39
+ // deep equal entities while merging state
40
+ // support multiple stores
41
+ // add validation if entity is full enough
42
+ // optimistic response
43
+ // make error type generic
44
+ // proper types, remove as, any, todo
45
+ // ! low
46
+ // cancellation to queries
47
+ // if mutation & query alrady loading - make options: last, throttle, debounce, parallel?
48
+ // add time-to-live option, and/or time-to-refresh
49
+ // add getUpdateTime option to check entities while merging
50
+ // useLocalMutation - uses local component state, or make store option - redux or component state or context? 1 version: redux only
51
+ // replace try/catch with returned error
52
+ // support any store, not only redux
53
+ // QueryInfo.defaultOptions
54
+ // set options in refresh/mutate functions
55
+ // multiple reducers instead of 1?
56
+ // don't cache result if resultSelector set?
57
+ __exportStar(require("./reducer"), exports);
58
+ __exportStar(require("./types"), exports);
59
+ __exportStar(require("./useMutation"), exports);
60
+ __exportStar(require("./useQuery"), exports);
61
+ __exportStar(require("./utilsAndConstants"), exports);
62
+ /** Creates reducer, actions and hooks for managing queries and mutations through redux cache. */
63
+ const createCache = (cache) => {
64
+ var _a, _b, _c, _d;
65
+ var _e, _f, _g;
66
+ // @ts-expect-error hot
67
+ const hotReloadEnabled = Boolean(module === null || module === void 0 ? void 0 : module.hot);
68
+ // provide all optional fields
69
+ (_a = cache.options) !== null && _a !== void 0 ? _a : (cache.options = {});
70
+ (_b = (_e = cache.options).logsEnabled) !== null && _b !== void 0 ? _b : (_e.logsEnabled = false);
71
+ (_c = (_f = cache.options).validateFunctionArguments) !== null && _c !== void 0 ? _c : (_f.validateFunctionArguments = utilsAndConstants_1.isDev);
72
+ (_d = (_g = cache.options).validateHookArguments) !== null && _d !== void 0 ? _d : (_g.validateHookArguments = utilsAndConstants_1.isDev && !hotReloadEnabled);
73
+ const nonPartialCache = cache;
74
+ // make selectors
75
+ const entitiesSelector = (state) => {
76
+ return nonPartialCache.cacheStateSelector(state).entities;
77
+ };
78
+ const enitityMapSelectorByTypename = Object.keys(cache.typenames).reduce((result, x) => {
79
+ result[x] = (state) => nonPartialCache.cacheStateSelector(state).entities[x];
80
+ return result;
81
+ }, {});
82
+ return {
83
+ cache: nonPartialCache,
84
+ /** Reducer of the cache, should be added to redux store. */
85
+ reducer: (0, reducer_1.createCacheReducer)(nonPartialCache.typenames, nonPartialCache.queries, nonPartialCache.mutations, nonPartialCache.options),
86
+ actions: {
87
+ /** Updates query state, and optionally merges entity changes in a single action. */
88
+ setQueryStateAndEntities: reducer_1.setQueryStateAndEntities,
89
+ /** Updates mutation state, and optionally merges entity changes in a single action. */
90
+ setMutationStateAndEntities: reducer_1.setMutationStateAndEntities,
91
+ /** Merge EntityChanges to the state. */
92
+ mergeEntityChanges: reducer_1.mergeEntityChanges,
93
+ },
94
+ selectors: {
95
+ entitiesSelector,
96
+ entitiesByTypenameSelector: (typename) => {
97
+ return enitityMapSelectorByTypename[typename];
98
+ },
99
+ },
100
+ hooks: {
101
+ /** Fetches query when params change and subscribes to query state. */
102
+ useQuery: (options) => (0, useQuery_1.useQuery)(nonPartialCache, options),
103
+ /** Subscribes to provided mutation state and provides mutate function. */
104
+ useMutation: (options) => (0, useMutation_1.useMutation)(nonPartialCache, options),
105
+ /** Selects entity by id and subscribes to the changes. */
106
+ useSelectEntityById: (id, typename) => {
107
+ return (0, react_redux_1.useSelector)((state) => id == null ? undefined : nonPartialCache.cacheStateSelector(state).entities[typename][id]);
108
+ },
109
+ },
110
+ };
111
+ };
112
+ exports.createCache = createCache;
@@ -0,0 +1,46 @@
1
+ import { Cache, Dict, EntitiesMap, EntityChanges, Key, QueryMutationState, Typenames } from './types';
2
+ export type ReduxCacheState<T extends Typenames, QP, QR, MP, MR> = ReturnType<ReturnType<typeof createCacheReducer<T, QP, QR, MP, MR>>>;
3
+ export declare const createCacheReducer: <T extends Typenames, QP, QR, MP, MR>(typenames: T, queries: QP & QR extends infer T_1 ? { [QK in keyof T_1]: QK extends keyof QP & keyof QR ? import("./types").QueryInfo<T, QP[QK], QR[QK], {
4
+ entities: EntitiesMap<T>;
5
+ queries: { [QK_1 in keyof QR]: Dict<QueryMutationState<QR[QK_1]>>; };
6
+ mutations: { [MK in keyof MR]: QueryMutationState<MR[MK]>; };
7
+ }> : never; } : never, mutations: MP & MR extends infer T_2 ? { [MK_1 in keyof T_2]: MK_1 extends keyof MP & keyof MR ? import("./types").MutationInfo<T, MP[MK_1], MR[MK_1]> : never; } : never, cacheOptions: import("./types").CacheOptions) => (state: {
8
+ entities: EntitiesMap<T>;
9
+ queries: { [QK_1 in keyof QR]: Dict<QueryMutationState<QR[QK_1]>>; };
10
+ mutations: { [MK in keyof MR]: QueryMutationState<MR[MK]>; };
11
+ } | undefined, action: {
12
+ type: `${string}SET_QUERY_STATE_AND_ENTITIES`;
13
+ queryKey: keyof QR;
14
+ queryCacheKey: Key;
15
+ state: Partial<QueryMutationState<QR[keyof QR]>> | undefined;
16
+ entityChagnes: EntityChanges<T> | undefined;
17
+ } | {
18
+ type: `${string}SET_MUTATION_STATE_AND_ENTITIES`;
19
+ mutationKey: keyof MR;
20
+ state: Partial<QueryMutationState<MR[keyof MR]>> | undefined;
21
+ entityChagnes: EntityChanges<T> | undefined;
22
+ } | {
23
+ type: `${string}MERGE_ENTITY_CHANGES`;
24
+ changes: EntityChanges<T>;
25
+ }) => {
26
+ entities: EntitiesMap<T>;
27
+ queries: { [QK_1 in keyof QR]: Dict<QueryMutationState<QR[QK_1]>>; };
28
+ mutations: { [MK in keyof MR]: QueryMutationState<MR[MK]>; };
29
+ };
30
+ export declare const setQueryStateAndEntities: <T extends Typenames, QR, K extends keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QR[K]>> | undefined, entityChagnes?: EntityChanges<T> | undefined) => {
31
+ type: `${string}SET_QUERY_STATE_AND_ENTITIES`;
32
+ queryKey: K;
33
+ queryCacheKey: Key;
34
+ state: Partial<QueryMutationState<QR[K]>> | undefined;
35
+ entityChagnes: EntityChanges<T> | undefined;
36
+ };
37
+ export declare const setMutationStateAndEntities: <T extends Typenames, MR, K extends keyof MR>(mutationKey: K, state?: Partial<QueryMutationState<MR[K]>> | undefined, entityChagnes?: EntityChanges<T> | undefined) => {
38
+ type: `${string}SET_MUTATION_STATE_AND_ENTITIES`;
39
+ mutationKey: K;
40
+ state: Partial<QueryMutationState<MR[K]>> | undefined;
41
+ entityChagnes: EntityChanges<T> | undefined;
42
+ };
43
+ export declare const mergeEntityChanges: <T extends Typenames>(changes: EntityChanges<T>) => {
44
+ type: `${string}MERGE_ENTITY_CHANGES`;
45
+ changes: EntityChanges<T>;
46
+ };
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeEntityChanges = exports.setMutationStateAndEntities = exports.setQueryStateAndEntities = exports.createCacheReducer = void 0;
4
+ const utilsAndConstants_1 = require("./utilsAndConstants");
5
+ const createCacheReducer = (typenames, queries, mutations, cacheOptions) => {
6
+ const entitiesMap = {};
7
+ for (const key in typenames) {
8
+ entitiesMap[key] = {};
9
+ }
10
+ const queriesMap = {};
11
+ for (const key in queries) {
12
+ queriesMap[key] = {};
13
+ }
14
+ const mutationsMap = {};
15
+ const initialState = {
16
+ entities: entitiesMap,
17
+ queries: queriesMap,
18
+ mutations: mutationsMap,
19
+ };
20
+ cacheOptions.logsEnabled &&
21
+ (0, utilsAndConstants_1.log)('createCacheReducer', {
22
+ typenames,
23
+ queries,
24
+ mutations,
25
+ initialState,
26
+ });
27
+ return (state = initialState, action) => {
28
+ switch (action.type) {
29
+ case '@RRQN/SET_QUERY_STATE_AND_ENTITIES': {
30
+ const { queryKey, queryCacheKey, state: queryState, entityChagnes } = action;
31
+ const newEntities = entityChagnes && (0, utilsAndConstants_1.processEntityChanges)(state.entities, entityChagnes, cacheOptions);
32
+ if (!queryState && !newEntities) {
33
+ return state;
34
+ }
35
+ 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({}, state.queries[queryKey][queryCacheKey]), queryState) }) }) });
36
+ }
37
+ case '@RRQN/SET_MUTATION_STATE_AND_ENTITIES': {
38
+ const { mutationKey, state: mutationState, entityChagnes } = action;
39
+ const newEntities = entityChagnes && (0, utilsAndConstants_1.processEntityChanges)(state.entities, entityChagnes, cacheOptions);
40
+ if (!mutationState && !newEntities) {
41
+ return state;
42
+ }
43
+ return Object.assign(Object.assign(Object.assign({}, state), (newEntities ? { entities: newEntities } : null)), { mutations: Object.assign(Object.assign({}, state.mutations), { [mutationKey]: Object.assign(Object.assign({}, state.mutations[mutationKey]), mutationState) }) });
44
+ }
45
+ case '@RRQN/MERGE_ENTITY_CHANGES': {
46
+ const { changes } = action;
47
+ const newEntities = (0, utilsAndConstants_1.processEntityChanges)(state.entities, changes, cacheOptions);
48
+ return newEntities ? Object.assign(Object.assign({}, state), { entities: newEntities }) : state;
49
+ }
50
+ }
51
+ return state;
52
+ };
53
+ };
54
+ exports.createCacheReducer = createCacheReducer;
55
+ const actionPrefix = `@${utilsAndConstants_1.PACKAGE_SHORT_NAME}/`;
56
+ const setQueryStateAndEntities = (queryKey, queryCacheKey, state, entityChagnes) => ({
57
+ type: `${actionPrefix}SET_QUERY_STATE_AND_ENTITIES`,
58
+ queryKey,
59
+ queryCacheKey,
60
+ state,
61
+ entityChagnes,
62
+ });
63
+ exports.setQueryStateAndEntities = setQueryStateAndEntities;
64
+ const setMutationStateAndEntities = (mutationKey, state, entityChagnes) => ({
65
+ type: `${actionPrefix}SET_MUTATION_STATE_AND_ENTITIES`,
66
+ mutationKey,
67
+ state,
68
+ entityChagnes,
69
+ });
70
+ exports.setMutationStateAndEntities = setMutationStateAndEntities;
71
+ const mergeEntityChanges = (changes) => ({
72
+ type: `${actionPrefix}MERGE_ENTITY_CHANGES`,
73
+ changes,
74
+ });
75
+ exports.mergeEntityChanges = mergeEntityChanges;
@@ -0,0 +1,137 @@
1
+ import type { createCacheReducer, ReduxCacheState } from './reducer';
2
+ export type Key = string | number | symbol;
3
+ export type Dict<T> = Record<Key, T>;
4
+ export type OptionalPartial<T, K extends keyof T> = Partial<{
5
+ [A in K]: Partial<T[A]>;
6
+ }> & Omit<T, K>;
7
+ /** Entity changes to be merged to redux state. */
8
+ export type EntityChanges<T extends Typenames> = {
9
+ /** Entities that will be merged with existing. */
10
+ merge?: Partial<PartialEntitiesMap<T>>;
11
+ /** Entities that will replace existing. */
12
+ replace?: Partial<EntitiesMap<T>>;
13
+ /** Ids of entities that will be removed. */
14
+ remove?: Partial<EntityIds<T>>;
15
+ /** Alias for `merge` to support normalizr. */
16
+ entities?: EntityChanges<T>['merge'];
17
+ };
18
+ /** Record of typename and its corresponding entity type */
19
+ export type Typenames = Record<string, object>;
20
+ export type Cache<T extends Typenames, QP, QR, MP, MR> = {
21
+ typenames: T;
22
+ queries: {
23
+ [QK in keyof (QP & QR)]: QK extends keyof (QP | QR) ? QueryInfo<T, QP[QK], QR[QK], ReturnType<ReturnType<typeof createCacheReducer<T, QP, QR, MP, MR>>>> : never;
24
+ };
25
+ mutations: {
26
+ [MK in keyof (MP & MR)]: MK extends keyof (MP | MR) ? MutationInfo<T, MP[MK], MR[MK]> : never;
27
+ };
28
+ options: CacheOptions;
29
+ /** Returns cache state from redux root state. */
30
+ cacheStateSelector: (state: any) => ReduxCacheState<T, QP, QR, MP, MR>;
31
+ };
32
+ export type CacheOptions = {
33
+ /**
34
+ * Enables validation of package function arguments. Recommened to enable in dev/testing mode.
35
+ * Default is true in dev mode.
36
+ * */
37
+ validateFunctionArguments: boolean;
38
+ /**
39
+ * Enables validation of package hook arguments. Recommened to enable in dev/testing mode and disable in production.
40
+ * Should be disabled with hot reloading.
41
+ * Default is true in dev mode without hot reloading.
42
+ * */
43
+ validateHookArguments: boolean;
44
+ /**
45
+ * Enable console logs.
46
+ * Default is false.
47
+ */
48
+ logsEnabled: boolean;
49
+ };
50
+ export type PartialEntitiesMap<T extends Typenames> = {
51
+ [K in keyof T]: Dict<Partial<T[K]>>;
52
+ };
53
+ export type EntitiesMap<T extends Typenames> = {
54
+ [K in keyof T]: Dict<T[K] | undefined>;
55
+ };
56
+ export type EntityIds<T extends Typenames> = {
57
+ [K in keyof T]: Key[];
58
+ };
59
+ export type Query<T extends Typenames, P, R> = (params: P) => Promise<QueryResponse<T, R>>;
60
+ export type QueryInfo<T extends Typenames, P, R, S> = {
61
+ query: Query<T, P, R>;
62
+ /**
63
+ * Cache policy string or cache options object.
64
+ * Default is { policy: 'cache-first', cacheQueryState: true, cacheEntities: true }
65
+ * @param cache-first for each params key fetch is not called if cache exists.
66
+ * @param cache-and-fetch for each params key result is taken from cache and fetch is called.
67
+ */
68
+ cacheOptions?: QueryCacheOptions | QueryCachePolicy;
69
+ /**
70
+ * Selector for query result from redux state.
71
+ * Can prevent hook from doing unnecessary fetches.
72
+ * Needed when query result may already be in the cache, e.g. for single entity query by id.
73
+ * */
74
+ resultSelector?: (state: S, params: P) => R | undefined;
75
+ /** Merges results before saving to the store. */
76
+ mergeResults?: (oldResult: R | undefined, response: QueryResponse<T, R>, params: P | undefined) => R;
77
+ /**
78
+ * Params key is used for determining if parameters were changed and fetch is needed.
79
+ * Also used as cache key, of `getCacheKey` wasn't provided.
80
+ * Default implementation uses `JSON.stringify` of parameters.
81
+ * */
82
+ getParamsKey?: (params?: P) => Key;
83
+ /**
84
+ * Cache key is a key in redux state for caching query state.
85
+ * Queries with equal cache keys have the same state.
86
+ * Default implementation is equal to `getParamsKey`.
87
+ * */
88
+ getCacheKey?: (params?: P) => Key;
89
+ };
90
+ export type UseQueryOptions<T extends Typenames, QP, QR, MP, MR, QK extends keyof (QP & QR)> = {
91
+ query: QK;
92
+ params: QK extends keyof (QP | QR) ? QP[QK] : never;
93
+ skip?: boolean;
94
+ } & Pick<QK extends keyof (QP | QR) ? QueryInfo<T, QP[QK], QR[QK], ReduxCacheState<T, QP, QR, MP, MR>> : never, 'cacheOptions' | 'mergeResults' | 'getCacheKey'>;
95
+ /**
96
+ * @param cache-first for each params key fetch is not called if cache exists.
97
+ * @param cache-and-fetch for each params key result is taken from cache and fetch is called.
98
+ */
99
+ export type QueryCachePolicy = 'cache-first' | 'cache-and-fetch';
100
+ export type QueryCacheOptions = {
101
+ /**
102
+ * @param cache-first for each params key fetch is not called if cache exists.
103
+ * @param cache-and-fetch for each params key result is taken from cache and fetch is called.
104
+ */
105
+ policy: QueryCachePolicy;
106
+ /** If `false`, query state is not saved in the store. Default is `true`. */
107
+ cacheQueryState: boolean;
108
+ /** If `false`, entities from response are not saved to the store. Default is `true`. */
109
+ cacheEntities: boolean;
110
+ };
111
+ export type QueryResponse<T extends Typenames, R> = EntityChanges<T> & {
112
+ /** Normalized result of a query. */
113
+ result: R;
114
+ };
115
+ export type Mutation<T extends Typenames, P, R> = (params: P,
116
+ /** Signal is aborted for current mutation when the same mutation was called once again. */
117
+ abortSignal: AbortSignal) => Promise<MutationResponse<T, R>>;
118
+ export type MutationInfo<T extends Typenames, P, R> = {
119
+ mutation: Mutation<T, P, R>;
120
+ cacheOptions?: MutationCacheOptions;
121
+ };
122
+ export type MutationCacheOptions = Pick<QueryCacheOptions, 'cacheEntities'> & {
123
+ /** If `false`, mutation state is not saved in the store. Default is `true`. */
124
+ cacheMutationState: boolean;
125
+ };
126
+ export type MutationResponse<T extends Typenames, R> = EntityChanges<T> & {
127
+ /** Normalized result of a mutation. */
128
+ result?: R;
129
+ };
130
+ export type QueryMutationState<R> = {
131
+ /** `true` when query or mutation is currently in progress. */
132
+ loading: boolean;
133
+ /** Result of the latest successfull query response. */
134
+ result?: R;
135
+ /** Error of the latest response. */
136
+ error?: Error;
137
+ };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // Common
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { Cache, MutationCacheOptions, QueryMutationState, Typenames } from './types';
2
+ export declare const DEFAULT_MUTATION_CACHE_OPTIONS: MutationCacheOptions;
3
+ export declare const useMutation: <T extends Typenames, MP, MR, MK extends keyof MP | keyof MR>(cache: Cache<T, unknown, unknown, MP, MR>, options: {
4
+ mutation: MK;
5
+ cacheOptions?: MutationCacheOptions | undefined;
6
+ }) => readonly [(params: MK extends keyof MP & keyof MR ? MP[MK] : never) => Promise<void>, QueryMutationState<MK extends keyof MP & keyof MR ? MP[MK] : never>, AbortController | undefined];
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.useMutation = exports.DEFAULT_MUTATION_CACHE_OPTIONS = void 0;
13
+ const react_1 = require("react");
14
+ const react_redux_1 = require("react-redux");
15
+ const reducer_1 = require("./reducer");
16
+ const utilsAndConstants_1 = require("./utilsAndConstants");
17
+ exports.DEFAULT_MUTATION_CACHE_OPTIONS = {
18
+ cacheMutationState: true,
19
+ cacheEntities: true,
20
+ };
21
+ const useMutation = (cache, options) => {
22
+ var _a, _b;
23
+ const { mutation: mutationKey, cacheOptions = (_a = cache.mutations[mutationKey].cacheOptions) !== null && _a !== void 0 ? _a : exports.DEFAULT_MUTATION_CACHE_OPTIONS, } = options;
24
+ const dispatch = (0, react_redux_1.useDispatch)();
25
+ cache.options.logsEnabled &&
26
+ (0, utilsAndConstants_1.log)('useMutation', {
27
+ cacheOptions,
28
+ });
29
+ // Check values that should be set once.
30
+ // Can be removed from deps.
31
+ cache.options.validateHookArguments &&
32
+ (() => {
33
+ ;
34
+ [
35
+ ['cache', cache],
36
+ ['cache.options', cache.options],
37
+ ['cache.options.logsEnabled', cache.options.logsEnabled],
38
+ ['cacheStateSelector', cache.cacheStateSelector],
39
+ ['mutationKey', mutationKey],
40
+ ['cacheOptions.cacheEntities', cacheOptions.cacheEntities],
41
+ ['cacheOptions.cacheMutationState', cacheOptions.cacheMutationState],
42
+ ]
43
+ // eslint-disable-next-line react-hooks/rules-of-hooks
44
+ .forEach((args) => (0, utilsAndConstants_1.useAssertValueNotChanged)(...args));
45
+ })();
46
+ const abortControllerRef = (0, react_1.useRef)();
47
+ const mutationStateSelector = (0, react_1.useCallback)((state) => {
48
+ cache.options.logsEnabled &&
49
+ (0, utilsAndConstants_1.log)('mutationStateSelector', {
50
+ state,
51
+ cacheState: cache.cacheStateSelector(state),
52
+ });
53
+ return cache.cacheStateSelector(state).mutations[mutationKey];
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
+ }, []);
56
+ // @ts-expect-error fix later
57
+ const mutationState = (_b = (0, react_redux_1.useSelector)(mutationStateSelector)) !== null && _b !== void 0 ? _b : utilsAndConstants_1.defaultEndpointState;
58
+ const mutate = (0, react_1.useCallback)((params) => __awaiter(void 0, void 0, void 0, function* () {
59
+ cache.options.logsEnabled &&
60
+ (0, utilsAndConstants_1.log)('mutate', {
61
+ mutationKey,
62
+ params,
63
+ abortController: abortControllerRef.current,
64
+ });
65
+ if (abortControllerRef.current) {
66
+ abortControllerRef.current.abort();
67
+ }
68
+ else {
69
+ cacheOptions.cacheMutationState &&
70
+ dispatch((0, reducer_1.setMutationStateAndEntities)(mutationKey, { loading: true }));
71
+ }
72
+ const abortController = new AbortController();
73
+ abortControllerRef.current = abortController;
74
+ let response;
75
+ let error;
76
+ const fetchFn = cache.mutations[mutationKey].mutation;
77
+ try {
78
+ response = yield fetchFn(
79
+ // @ts-expect-error fix later
80
+ params, abortController.signal);
81
+ }
82
+ catch (e) {
83
+ error = e;
84
+ }
85
+ cache.options.logsEnabled &&
86
+ (0, utilsAndConstants_1.log)('mutate finished', {
87
+ response,
88
+ error,
89
+ aborted: abortController.signal.aborted,
90
+ });
91
+ if (abortController.signal.aborted) {
92
+ return;
93
+ }
94
+ abortControllerRef.current = undefined;
95
+ if (response) {
96
+ dispatch((0, reducer_1.setMutationStateAndEntities)(mutationKey, cacheOptions.cacheMutationState
97
+ ? {
98
+ error: undefined,
99
+ loading: false,
100
+ result: response.result,
101
+ }
102
+ : undefined, cacheOptions.cacheEntities ? response : undefined));
103
+ }
104
+ else if (error && cacheOptions.cacheMutationState) {
105
+ dispatch((0, reducer_1.setMutationStateAndEntities)(mutationKey, {
106
+ error: error,
107
+ loading: false,
108
+ }));
109
+ }
110
+ }),
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
+ []);
113
+ return [mutate, mutationState, abortControllerRef.current];
114
+ };
115
+ exports.useMutation = useMutation;
@@ -0,0 +1,8 @@
1
+ import { Cache, QueryCacheOptions, QueryCachePolicy, QueryMutationState, Typenames, UseQueryOptions } from './types';
2
+ export declare const QUERY_CACHE_OPTIONS_BY_POLICY: Record<QueryCachePolicy, QueryCacheOptions>;
3
+ export declare const DEFAULT_QUERY_CACHE_OPTIONS: {
4
+ readonly policy: "cache-first";
5
+ readonly cacheQueryState: true;
6
+ readonly cacheEntities: true;
7
+ };
8
+ export declare const useQuery: <T extends Typenames, QP, QR, MP, MR, QK extends keyof QP | keyof QR>(cache: Cache<T, QP, QR, MP, MR>, options: UseQueryOptions<T, QP, QR, MP, MR, QK>) => readonly [QueryMutationState<QK extends keyof QP & keyof QR ? QR[QK] : never>, (params?: (QK extends keyof QP & keyof QR ? QP[QK] : never) | undefined) => Promise<void>];
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.useQuery = exports.DEFAULT_QUERY_CACHE_OPTIONS = exports.QUERY_CACHE_OPTIONS_BY_POLICY = void 0;
13
+ const react_1 = require("react");
14
+ const react_redux_1 = require("react-redux");
15
+ const reducer_1 = require("./reducer");
16
+ const utilsAndConstants_1 = require("./utilsAndConstants");
17
+ const CACHE_FIRST_OPTIONS = {
18
+ policy: 'cache-first',
19
+ cacheQueryState: true,
20
+ cacheEntities: true,
21
+ };
22
+ exports.QUERY_CACHE_OPTIONS_BY_POLICY = {
23
+ 'cache-first': CACHE_FIRST_OPTIONS,
24
+ 'cache-and-fetch': Object.assign(Object.assign({}, CACHE_FIRST_OPTIONS), { policy: 'cache-and-fetch' }),
25
+ };
26
+ exports.DEFAULT_QUERY_CACHE_OPTIONS = CACHE_FIRST_OPTIONS;
27
+ const useQuery = (cache, options) => {
28
+ var _a, _b, _c, _d;
29
+ const getParamsKey = (_a = cache.queries[options.query].getParamsKey) !== null && _a !== void 0 ? _a : (utilsAndConstants_1.defaultGetParamsKey);
30
+ const { query: queryKey, skip, params: hookParams, cacheOptions: cacheOptionsOrPolicy = (_b = cache.queries[queryKey].cacheOptions) !== null && _b !== void 0 ? _b : exports.DEFAULT_QUERY_CACHE_OPTIONS, mergeResults = cache.queries[queryKey].mergeResults, getCacheKey = (_c = cache.queries[queryKey].getCacheKey) !== null && _c !== void 0 ? _c : getParamsKey, } = options;
31
+ const hookParamsKey = getParamsKey(
32
+ // @ts-expect-error fix later
33
+ hookParams);
34
+ const cacheOptions = typeof cacheOptionsOrPolicy === 'string'
35
+ ? exports.QUERY_CACHE_OPTIONS_BY_POLICY[cacheOptionsOrPolicy]
36
+ : cacheOptionsOrPolicy;
37
+ const store = (0, react_redux_1.useStore)();
38
+ // Check values that should be set once.
39
+ cache.options.validateHookArguments &&
40
+ (() => {
41
+ ;
42
+ [
43
+ ['store', store],
44
+ ['cache', cache],
45
+ ['cache.queries', cache.queries],
46
+ ['cacheStateSelector', cache.cacheStateSelector],
47
+ ['cacheOptions.cacheEntities', cacheOptions.cacheEntities],
48
+ ['cacheOptions.cacheQueryState', cacheOptions.cacheQueryState],
49
+ ['options.query', options.query],
50
+ ['queryKey', queryKey],
51
+ ['resultSelector', cache.queries[queryKey].resultSelector],
52
+ ]
53
+ // eslint-disable-next-line react-hooks/rules-of-hooks
54
+ .forEach((args) => (0, utilsAndConstants_1.useAssertValueNotChanged)(...args));
55
+ })();
56
+ const forceUpdate = (0, utilsAndConstants_1.useForceUpdate)();
57
+ // Keeps most of local state.
58
+ // Reference because state is changed not only by changing hook arguments, but also by calling fetch, and it should be done synchronously.
59
+ const stateRef = (0, react_1.useRef)({});
60
+ (0, react_1.useMemo)(() => {
61
+ if (skip || stateRef.current.paramsKey === hookParamsKey) {
62
+ return;
63
+ }
64
+ const resultSelectorImpl = cache.queries[queryKey].resultSelector;
65
+ const state = stateRef.current;
66
+ state.params = hookParams;
67
+ state.paramsKey = hookParamsKey;
68
+ state.latestHookParamsKey = state.paramsKey;
69
+ // @ts-expect-error fix later
70
+ state.cacheKey = getCacheKey(hookParams);
71
+ state.resultSelector = createResultSelector(
72
+ // @ts-expect-error fix later
73
+ resultSelectorImpl, cache.cacheStateSelector, hookParams);
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ }, [hookParamsKey, skip]);
76
+ const resultFromSelector =
77
+ // eslint-disable-next-line react-hooks/rules-of-hooks
78
+ stateRef.current.resultSelector && (0, react_redux_1.useSelector)(stateRef.current.resultSelector);
79
+ const hasResultFromSelector = resultFromSelector !== undefined;
80
+ const queryStateSelector = (0, react_1.useCallback)((state) => {
81
+ cache.options.logsEnabled &&
82
+ (0, utilsAndConstants_1.log)('queryStateSelector', {
83
+ state,
84
+ queryKey,
85
+ cacheKey: stateRef.current.cacheKey,
86
+ cacheState: cache.cacheStateSelector(state),
87
+ });
88
+ const queryState = cache.cacheStateSelector(state).queries[queryKey][stateRef.current.cacheKey];
89
+ return queryState; // TODO proper type
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ }, []);
92
+ const queryStateFromSelector = (_d = (0, react_redux_1.useSelector)(queryStateSelector)) !== null && _d !== void 0 ? _d : utilsAndConstants_1.defaultEndpointState;
93
+ const queryState = hasResultFromSelector
94
+ ? (Object.assign(Object.assign({}, queryStateFromSelector), { result: resultFromSelector }))
95
+ : queryStateFromSelector;
96
+ const fetchImpl = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
97
+ var _e;
98
+ cache.options.logsEnabled && (0, utilsAndConstants_1.log)('useQuery.fetchImpl', { queryState });
99
+ if (queryState.loading) {
100
+ return;
101
+ }
102
+ cacheOptions.cacheQueryState &&
103
+ store.dispatch((0, reducer_1.setQueryStateAndEntities)(queryKey, stateRef.current.cacheKey, {
104
+ loading: true,
105
+ }));
106
+ const { paramsKey, params } = stateRef.current;
107
+ let response;
108
+ const fetchFn = cache.queries[queryKey].query;
109
+ try {
110
+ response = yield fetchFn(
111
+ // @ts-expect-error fix later
112
+ params);
113
+ }
114
+ catch (error) {
115
+ if (stateRef.current.paramsKey === paramsKey && cacheOptions.cacheQueryState) {
116
+ store.dispatch((0, reducer_1.setQueryStateAndEntities)(queryKey, stateRef.current.cacheKey, {
117
+ error: error,
118
+ loading: false,
119
+ }));
120
+ }
121
+ }
122
+ if (response && stateRef.current.paramsKey === paramsKey) {
123
+ store.dispatch((0, reducer_1.setQueryStateAndEntities)(queryKey, stateRef.current.cacheKey, !cacheOptions.cacheQueryState
124
+ ? undefined
125
+ : {
126
+ error: undefined,
127
+ loading: false,
128
+ result: hasResultFromSelector
129
+ ? undefined
130
+ : mergeResults
131
+ ? mergeResults(
132
+ // @ts-expect-error fix later
133
+ (_e = queryStateSelector(store.getState())) === null || _e === void 0 ? void 0 : _e.result, response, params)
134
+ : response.result,
135
+ }, cacheOptions.cacheEntities ? response : undefined));
136
+ }
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }), [mergeResults, queryState.loading, hasResultFromSelector]);
139
+ (0, react_1.useEffect)(() => {
140
+ if (queryState.result != null && cacheOptions.policy === 'cache-first') {
141
+ return;
142
+ }
143
+ fetchImpl();
144
+ // eslint-disable-next-line react-hooks/exhaustive-deps
145
+ }, [stateRef.current.latestHookParamsKey]);
146
+ const fetch = (0, react_1.useCallback)((params) => {
147
+ cache.options.logsEnabled && (0, utilsAndConstants_1.log)('useQuery.fetch', params);
148
+ if (params !== undefined) {
149
+ const state = stateRef.current;
150
+ // @ts-expect-error fix later
151
+ const paramsKey = getParamsKey(params);
152
+ if (state.paramsKey !== paramsKey) {
153
+ const resultSelectorImpl = cache.queries[queryKey].resultSelector;
154
+ state.params = params;
155
+ state.paramsKey = paramsKey;
156
+ // @ts-expect-error fix later
157
+ state.cacheKey = getCacheKey(params);
158
+ state.resultSelector = createResultSelector(
159
+ // @ts-expect-error fix later
160
+ resultSelectorImpl, cache.cacheStateSelector, hookParams);
161
+ forceUpdate();
162
+ }
163
+ }
164
+ return fetchImpl();
165
+ },
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ [fetchImpl, getCacheKey]);
168
+ return [queryState, fetch];
169
+ };
170
+ exports.useQuery = useQuery;
171
+ const createResultSelector = (resultSelector, cacheStateSelector, params) => {
172
+ return (resultSelector && ((state) => resultSelector(cacheStateSelector(state), params)));
173
+ };
@@ -0,0 +1,19 @@
1
+ /// <reference types="react" />
2
+ import { CacheOptions, EntitiesMap, EntityChanges, Typenames } from './types';
3
+ export declare const PACKAGE_SHORT_NAME = "RRC";
4
+ export declare const isDev: boolean;
5
+ export declare const defaultEndpointState: {
6
+ readonly loading: false;
7
+ };
8
+ export declare const defaultGetParamsKey: <P = unknown>(params: P) => string;
9
+ /**
10
+ * @returns function to force update a function component.
11
+ */
12
+ export declare const useForceUpdate: () => import("react").DispatchWithoutAction;
13
+ export declare const useAssertValueNotChanged: (name: string, value: unknown) => void;
14
+ export declare const log: (tag: string, data: unknown) => void;
15
+ /**
16
+ * Process changes to entities map.
17
+ * @return `undefined` if nothing to change, otherwise processed entities map.
18
+ */
19
+ export declare const processEntityChanges: <T extends Typenames>(entities: EntitiesMap<T>, changes: EntityChanges<T>, options: CacheOptions) => EntitiesMap<T> | undefined;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processEntityChanges = exports.log = exports.useAssertValueNotChanged = exports.useForceUpdate = exports.defaultGetParamsKey = exports.defaultEndpointState = exports.isDev = exports.PACKAGE_SHORT_NAME = void 0;
4
+ const react_1 = require("react");
5
+ const react_2 = require("react");
6
+ exports.PACKAGE_SHORT_NAME = 'RRC';
7
+ exports.isDev = (() => {
8
+ try {
9
+ // @ts-expect-error __DEV__ is only for React Native
10
+ return __DEV__;
11
+ }
12
+ catch (e) {
13
+ return process.env.NODE_ENV === 'development';
14
+ }
15
+ })();
16
+ exports.defaultEndpointState = { loading: false };
17
+ const defaultGetParamsKey = (params) => !params ? '' : JSON.stringify(params);
18
+ exports.defaultGetParamsKey = defaultGetParamsKey;
19
+ const forceUpdateReducer = (i) => i + 1;
20
+ /**
21
+ * @returns function to force update a function component.
22
+ */
23
+ const useForceUpdate = () => {
24
+ return (0, react_2.useReducer)(forceUpdateReducer, 0)[1];
25
+ };
26
+ exports.useForceUpdate = useForceUpdate;
27
+ const useAssertValueNotChanged = (name, value) => {
28
+ const firstMountRef = (0, react_1.useRef)(false);
29
+ (0, react_1.useMemo)(() => {
30
+ if (firstMountRef.current) {
31
+ throw new Error(`${name} should not be modified`);
32
+ }
33
+ firstMountRef.current = true;
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [value]);
36
+ };
37
+ exports.useAssertValueNotChanged = useAssertValueNotChanged;
38
+ const log = (tag, data) => {
39
+ console.debug(`@${exports.PACKAGE_SHORT_NAME} [${tag}]`, data);
40
+ };
41
+ exports.log = log;
42
+ /**
43
+ * Process changes to entities map.
44
+ * @return `undefined` if nothing to change, otherwise processed entities map.
45
+ */
46
+ const processEntityChanges = (entities, changes, options) => {
47
+ var _a, _b, _c;
48
+ const { merge = changes.entities, replace, remove } = changes;
49
+ if (!merge && !replace && !remove) {
50
+ return undefined;
51
+ }
52
+ if (options.validateFunctionArguments) {
53
+ // check for merge and entities both set
54
+ if (changes.merge && changes.entities) {
55
+ throw new Error('Response merge and entities should not be both set');
56
+ }
57
+ // check for key intersection
58
+ const mergeKeys = merge && Object.keys(merge);
59
+ const replaceKeys = replace && Object.keys(replace);
60
+ const removeKeys = remove && Object.keys(remove);
61
+ const keysSet = new Set(mergeKeys);
62
+ replaceKeys === null || replaceKeys === void 0 ? void 0 : replaceKeys.forEach((key) => keysSet.add(key));
63
+ removeKeys === null || removeKeys === void 0 ? void 0 : removeKeys.forEach((key) => keysSet.add(key));
64
+ const totalKeysInResponse = ((_a = mergeKeys === null || mergeKeys === void 0 ? void 0 : mergeKeys.length) !== null && _a !== void 0 ? _a : 0) + ((_b = replaceKeys === null || replaceKeys === void 0 ? void 0 : replaceKeys.length) !== null && _b !== void 0 ? _b : 0) + ((_c = removeKeys === null || removeKeys === void 0 ? void 0 : removeKeys.length) !== null && _c !== void 0 ? _c : 0);
65
+ if (keysSet.size !== totalKeysInResponse) {
66
+ throw new Error('Merge, replace and remove keys should not intersect');
67
+ }
68
+ }
69
+ let result;
70
+ for (const typename in entities) {
71
+ const entitiesToMerge = merge === null || merge === void 0 ? void 0 : merge[typename];
72
+ const entitiesToReplace = replace === null || replace === void 0 ? void 0 : replace[typename];
73
+ const entitiesToRemove = remove === null || remove === void 0 ? void 0 : remove[typename];
74
+ if (!entitiesToMerge && !entitiesToReplace && !entitiesToRemove) {
75
+ continue;
76
+ }
77
+ const newEntities = Object.assign({}, entities[typename]);
78
+ // remove
79
+ entitiesToRemove === null || entitiesToRemove === void 0 ? void 0 : entitiesToRemove.forEach((id) => delete newEntities[id]);
80
+ // replace
81
+ if (entitiesToReplace) {
82
+ for (const id in entitiesToReplace) {
83
+ newEntities[id] = entitiesToReplace[id];
84
+ }
85
+ }
86
+ // merge
87
+ if (entitiesToMerge) {
88
+ for (const id in entitiesToMerge) {
89
+ newEntities[id] = Object.assign(Object.assign({}, newEntities[id]), entitiesToMerge[id]);
90
+ }
91
+ }
92
+ result !== null && result !== void 0 ? result : (result = Object.assign({}, entities));
93
+ result[typename] = newEntities;
94
+ }
95
+ (0, exports.log)('processEntityChanges', {
96
+ entities,
97
+ changes,
98
+ result,
99
+ });
100
+ return result;
101
+ };
102
+ exports.processEntityChanges = processEntityChanges;
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "react-redux-cache",
3
+ "author": "Alexander Danilov",
4
+ "license": "MIT",
5
+ "version": "0.0.1",
6
+ "description": "Powerful data fetching and caching library that supports normalization, built on top of redux",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "scripts": {
10
+ "example": "yarn build && (cd example && yarn && yarn start)",
11
+ "clean": "rm -rf dist package.tgz",
12
+ "build": "yarn clean && tsc && yarn pack -f package.tgz",
13
+ "test": "yarn build && node node_modules/jest/bin/jest.js",
14
+ "prepublishOnly": "yarn test"
15
+ },
16
+ "files": [
17
+ "dist/*"
18
+ ],
19
+ "devDependencies": {
20
+ "@types/jest": "29.5.1",
21
+ "@types/node": "20.1.2",
22
+ "@types/redux-logger": "3.0.9",
23
+ "@typescript-eslint/eslint-plugin": "5.59.5",
24
+ "@typescript-eslint/parser": "5.59.5",
25
+ "eslint": "8.40.0",
26
+ "eslint-config-prettier": "8.8.0",
27
+ "eslint-plugin-prettier": "4.2.1",
28
+ "eslint-plugin-react": "7.33.2",
29
+ "eslint-plugin-react-hooks": "4.6.0",
30
+ "eslint-plugin-simple-import-sort": "10.0.0",
31
+ "jest": "29.5.0",
32
+ "prettier": "2.8.8",
33
+ "react": "18.2.0",
34
+ "react-dom": "18.2.0",
35
+ "react-redux": "8.0.5",
36
+ "redux": "4.2.1",
37
+ "redux-logger": "3.0.6",
38
+ "ts-jest": "29.1.0",
39
+ "typescript": "5.0.4"
40
+ },
41
+ "peerDependencies": {
42
+ "react": "^16",
43
+ "react-redux": "^4",
44
+ "redux": "^4"
45
+ },
46
+ "dependencies": {
47
+ "redux-persist": "6.0.0"
48
+ },
49
+ "keywords": [
50
+ "react",
51
+ "redux",
52
+ "cache",
53
+ "query",
54
+ "normalization"
55
+ ]
56
+ }