@choksheak/react-utils 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +3 -1
  2. package/package.json +39 -33
  3. package/sharedQuery.d.mts +68 -8
  4. package/sharedQuery.d.ts +67 -7
  5. package/sharedQuery.js +240 -79
  6. package/sharedQuery.mjs +228 -77
  7. package/src/sharedQuery.ts +338 -103
  8. package/src/utils/useDeepMemo.ts +19 -0
  9. package/utils/useDeepMemo.d.mts +4 -0
  10. package/utils/useDeepMemo.d.ts +2 -0
  11. package/utils/useDeepMemo.js +51 -0
  12. package/utils/useDeepMemo.mjs +16 -0
  13. package/docs/.nojekyll +0 -1
  14. package/docs/assets/hierarchy.js +0 -1
  15. package/docs/assets/highlight.css +0 -22
  16. package/docs/assets/icons.js +0 -18
  17. package/docs/assets/icons.svg +0 -1
  18. package/docs/assets/main.js +0 -60
  19. package/docs/assets/navigation.js +0 -1
  20. package/docs/assets/search.js +0 -1
  21. package/docs/assets/style.css +0 -1633
  22. package/docs/classes/sharedQuery.SharedQuery.html +0 -17
  23. package/docs/classes/sharedState.SharedState.html +0 -9
  24. package/docs/classes/utils_storage.InMemoryStorageAdapter.html +0 -9
  25. package/docs/classes/utils_storage.IndexedDbAdapter.html +0 -9
  26. package/docs/classes/utils_storage.LocalStorageAdapter.html +0 -9
  27. package/docs/functions/sharedQuery.sharedQuery.html +0 -5
  28. package/docs/functions/sharedQuery.useSharedQuery.html +0 -4
  29. package/docs/functions/sharedState.sharedState.html +0 -3
  30. package/docs/functions/sharedState.useSharedState.html +0 -3
  31. package/docs/functions/sharedState.useSharedStateSetter.html +0 -2
  32. package/docs/functions/sharedState.useSharedStateValue.html +0 -2
  33. package/docs/functions/utils_storage.getStorageAdapter.html +0 -1
  34. package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +0 -7
  35. package/docs/hierarchy.html +0 -1
  36. package/docs/index.html +0 -2
  37. package/docs/modules/fetcher.html +0 -1
  38. package/docs/modules/index.html +0 -1
  39. package/docs/modules/sharedQuery.html +0 -1
  40. package/docs/modules/sharedState.html +0 -1
  41. package/docs/modules/utils_storage.html +0 -1
  42. package/docs/modules/utils_stringify.html +0 -1
  43. package/docs/modules.html +0 -1
  44. package/docs/types/sharedQuery.FetchFn.html +0 -1
  45. package/docs/types/sharedQuery.PersistTo.html +0 -1
  46. package/docs/types/sharedQuery.QueryStateValue.html +0 -6
  47. package/docs/types/sharedQuery.SharedQueryOptions.html +0 -1
  48. package/docs/types/sharedQuery.SharedQueryState.html +0 -3
  49. package/docs/types/sharedQuery.UseQueryResult.html +0 -1
  50. package/docs/types/sharedState.SharedStateOptions.html +0 -1
  51. package/docs/types/utils_storage.StorageAdapter.html +0 -5
  52. package/docs/types/utils_storage.StorageOptions.html +0 -5
  53. package/docs/variables/fetcher.fetcher.html +0 -2
  54. package/docs/variables/fetcher.fetcherWithRetries.html +0 -2
  55. package/docs/variables/sharedQuery.SharedQueryDefaults.html +0 -2
  56. package/docs/variables/sharedState.SharedStateDefaults.html +0 -6
  57. package/src/utils/stringify.test.ts +0 -46
  58. package/utils/stringify.test.d.mts +0 -2
  59. package/utils/stringify.test.d.ts +0 -2
  60. package/utils/stringify.test.js +0 -69
  61. package/utils/stringify.test.mjs +0 -86
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Chok's React Utilities
1
+ # @choksheak/react-utils
2
2
 
3
3
  Miscellaneous React+TypeScript utilities with support for full tree-shaking.
4
+
5
+ Online docs: https://choksheak.github.io/react-utils/
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@choksheak/react-utils",
3
3
  "license": "The Unlicense",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
6
6
  "private": false,
7
7
  "main": "index.js",
@@ -10,38 +10,10 @@
10
10
  "files": [
11
11
  "**/*"
12
12
  ],
13
- "exports": {
14
- "./package.json": "./package.json",
15
- "./fetcher": {
16
- "types": "./fetcher.d.ts",
17
- "import": "./fetcher.js"
18
- },
19
- "./sharedQuery": {
20
- "types": "./sharedQuery.d.ts",
21
- "import": "./sharedQuery.js"
22
- },
23
- "./sharedState": {
24
- "types": "./sharedState.d.ts",
25
- "import": "./sharedState.js"
26
- }
27
- },
28
- "typesVersions": {
29
- "*": {
30
- "fetcher": [
31
- "fetcher.d.ts"
32
- ],
33
- "sharedQuery": [
34
- "sharedQuery.d.ts"
35
- ],
36
- "sharedState": [
37
- "sharedState.d.ts"
38
- ]
39
- }
40
- },
41
13
  "scripts": {
42
14
  "lint": "eslint .",
43
15
  "fix": "eslint . --fix",
44
- "tsc": "tsc --noEmit",
16
+ "tsc": "tsc --noEmit --allowSyntheticDefaultImports",
45
17
  "ft": "npm run fix && npm run tsc",
46
18
  "build": "tsup src/*",
47
19
  "test": "vitest run",
@@ -50,6 +22,7 @@
50
22
  "ls": "lint-staged",
51
23
  "docs": "rm -rf docs && typedoc `find src -name '*.ts' ! -name '*.test.ts'` --out docs --name 'Chok’s React Utilities'",
52
24
  "gen-types": "tsc --emitDeclarationOnly --declaration",
25
+ "gen-exports": "tsx scripts/gen-exports.ts",
53
26
  "bump": "tsx scripts/bump-version.ts",
54
27
  "bump:patch": "tsx scripts/bump-version.ts --patch",
55
28
  "bump:minor": "tsx scripts/bump-version.ts --minor",
@@ -57,7 +30,7 @@
57
30
  "pkg": "scripts/package.sh",
58
31
  "pub": "npm publish dist/ --access=public",
59
32
  "commit": "scripts/commit-release.sh",
60
- "release": "npm run ci && npm run bump && npm run pkg && npm run pub && npm run commit",
33
+ "release": "npm run ci && npm run bump && npm i && npm run pkg && npm run pub && npm run commit",
61
34
  "postinstall": "simple-git-hooks"
62
35
  },
63
36
  "simple-git-hooks": {
@@ -90,8 +63,8 @@
90
63
  "devDependencies": {
91
64
  "@eslint/compat": "^1.4.0",
92
65
  "@eslint/js": "^9.38.0",
93
- "@tanstack/react-query": "^5.90.5",
94
66
  "@types/jest": "^30.0.0",
67
+ "@types/lodash": "^4.17.20",
95
68
  "@types/react": "^19.2.2",
96
69
  "@types/react-dom": "^19.2.2",
97
70
  "@typescript-eslint/eslint-plugin": "^8.46.2",
@@ -107,6 +80,8 @@
107
80
  "globals": "^16.4.0",
108
81
  "jiti": "^2.6.1",
109
82
  "lint-staged": "^16.2.6",
83
+ "lodash": "^4.17.21",
84
+ "object-sizeof": "^2.6.5",
110
85
  "prettier": "^3.6.2",
111
86
  "react": "^19.2.0",
112
87
  "react-dom": "^19.2.0",
@@ -116,13 +91,44 @@
116
91
  "typedoc": "^0.28.14",
117
92
  "typescript": "^5.9.3",
118
93
  "typescript-eslint": "^8.46.2",
94
+ "use-deep-compare": "^1.3.0",
119
95
  "vitest": "^4.0.3",
120
96
  "wretch": "^3.0.2"
121
97
  },
122
98
  "peerDependencies": {
123
99
  "@choksheak/ts-utils": ">=0.2.9",
124
- "@tanstack/react-query": ">=5",
100
+ "lodash": ">=4.17.21",
101
+ "object-sizeof": ">=2.6.5",
125
102
  "react": ">=16",
103
+ "use-deep-compare": ">=1.3.0",
126
104
  "wretch": ">=3"
105
+ },
106
+ "exports": {
107
+ "./package.json": "./package.json",
108
+ "./fetcher": {
109
+ "types": "./fetcher.d.ts",
110
+ "import": "./fetcher.js"
111
+ },
112
+ "./sharedQuery": {
113
+ "types": "./sharedQuery.d.ts",
114
+ "import": "./sharedQuery.js"
115
+ },
116
+ "./sharedState": {
117
+ "types": "./sharedState.d.ts",
118
+ "import": "./sharedState.js"
119
+ }
120
+ },
121
+ "typesVersions": {
122
+ "*": {
123
+ "fetcher": [
124
+ "./fetcher.d.ts"
125
+ ],
126
+ "sharedQuery": [
127
+ "./sharedQuery.d.ts"
128
+ ],
129
+ "sharedState": [
130
+ "./sharedState.d.ts"
131
+ ]
132
+ }
127
133
  }
128
134
  }
package/sharedQuery.d.mts CHANGED
@@ -1,16 +1,37 @@
1
1
  import { SharedStateOptions, SharedState } from './sharedState.mjs';
2
2
  import './utils/storage.mjs';
3
3
 
4
- type FetchFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
4
+ type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
5
5
  type QueryStateValue<TData> = {
6
+ /**
7
+ * When this record was last updated. Used for LRU determination. If you want
8
+ * the data age, please use `dataUpdatedMs` instead.
9
+ */
10
+ lastUpdatedMs: number;
6
11
  loading: boolean;
7
12
  data?: TData;
8
13
  dataUpdatedMs?: number;
9
- error?: Error;
14
+ /**
15
+ * Use unknown type for error because we can't assume what type of variable
16
+ * will be thrown from the query function. We also don't want to add this
17
+ * type to QueryStateValue<TError> as it adds complexity to the type.
18
+ */
19
+ error?: unknown;
10
20
  errorUpdatedMs?: number;
11
21
  };
12
22
  type UseQueryResult<TData> = QueryStateValue<TData> & {
13
- refetch: () => Promise<TData>;
23
+ /**
24
+ * Cancels the inflight query (if any). Returns true if canceled.
25
+ */
26
+ abortCurrentQuery: (reason?: unknown) => boolean;
27
+ /**
28
+ * Manually refresh the data by doing a new query.
29
+ */
30
+ refetch: () => void;
31
+ /**
32
+ * If you got the data from somewhere else (e.g. an in-memory data update),
33
+ * just set it directly.
34
+ */
14
35
  setData: (data: TData, dataUpdatedMs?: number) => void;
15
36
  deleteData: () => void;
16
37
  };
@@ -22,28 +43,49 @@ type PersistTo = "localStorage" | "indexedDb";
22
43
  type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
23
44
  type SharedQueryOptions<TArgs extends unknown[], TData> = {
24
45
  queryName: string;
25
- queryFn: FetchFn<TArgs, TData>;
46
+ queryFn: QueryFn<TArgs, TData>;
26
47
  staleMs?: number;
27
48
  expiryMs?: number;
28
49
  refetchOnStale?: boolean;
29
50
  persistTo?: PersistTo;
51
+ maxSize?: number;
52
+ maxBytes?: number;
53
+ log?: (...args: unknown[]) => void;
30
54
  } & SharedStateOptions<SharedQueryState<TData>>;
31
55
  /** Users can override these values globally. */
32
56
  declare const SharedQueryDefaults: {
33
57
  staleMs: number;
34
58
  expiryMs: number;
59
+ refetchOnStale: boolean;
35
60
  persistTo: PersistTo | undefined;
61
+ maxSize: number;
62
+ maxBytes: number;
63
+ log: (...args: unknown[]) => void;
36
64
  };
37
65
  declare class SharedQuery<TArgs extends unknown[], TData> {
38
- private readonly inflightPromises;
66
+ private readonly inflightQueries;
39
67
  readonly queryState: SharedState<SharedQueryState<TData>>;
40
68
  readonly queryName: string;
41
69
  private readonly queryFn;
42
70
  readonly expiryMs: number;
43
71
  readonly staleMs: number;
44
72
  readonly refetchOnStale: boolean;
73
+ readonly maxSize: number;
74
+ readonly maxBytes: number;
75
+ readonly log: (...args: unknown[]) => void;
45
76
  constructor(options: SharedQueryOptions<TArgs, TData>);
77
+ /** Returns a key to identify requests based on the given args. */
46
78
  getQueryKey(args: TArgs): string;
79
+ /**
80
+ * Get the AbortController to abort the inflight query (if any).
81
+ *
82
+ * Note that once you get the controller, you can also get the signal easily
83
+ * using `controller.signal`. So if you need both the controller and the
84
+ * signal, just use this function.
85
+ */
86
+ getAbortController(queryKey: string): AbortController | null;
87
+ /** Get the AbortSignal to check for query abortions. */
88
+ getAbortSignal(queryKey: string): AbortSignal | null;
47
89
  private isStale;
48
90
  private isExpired;
49
91
  getCachedOrFetch(...args: TArgs): Promise<TData>;
@@ -54,9 +96,14 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
54
96
  /** Do a new fetch even when the data is already cached. */
55
97
  updateFromSource(...args: TArgs): Promise<TData>;
56
98
  /** Delete the cached data for one set of arguments. */
57
- deleteData(args: TArgs): void;
99
+ deleteData(queryKey: string): void;
58
100
  /** Delete all currently cached data & all inflight promises. */
59
101
  clear(): void;
102
+ /**
103
+ * Discard the oldest entries if the size exceeds the limit. The last
104
+ * remaining record cannot be deleted no matter what the limit is.
105
+ */
106
+ enforceSizeLimit(): void;
60
107
  }
61
108
  /**
62
109
  * Create a reusable shared query object that can be used to auto de-duplicate
@@ -64,13 +111,26 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
64
111
  *
65
112
  * Each `queryName` must be unique, and can only be declared once, most often
66
113
  * in the top level scope. The queryName is used for logging only.
114
+ *
115
+ * Example:
116
+ * ```
117
+ * export const usersQuery = sharedQuery({
118
+ * queryName: "users",
119
+ * queryFn: listUsers,
120
+ * persistTo: "indexedDb",
121
+ * staleMs: MS_PER_DAY,
122
+ * });
123
+ * ```
67
124
  */
68
125
  declare function sharedQuery<TArgs extends unknown[], TData>(options: SharedQueryOptions<TArgs, TData>): SharedQuery<TArgs, TData>;
69
126
  /**
70
127
  * React hook to make use of a shared query inside any React component.
71
128
  *
72
- * const users = useSharedQuery(usersQuery, ["123"]);
129
+ * Example:
130
+ * ```
131
+ * const users = useSharedQuery(usersQuery, ["123"]);
132
+ * ```
73
133
  */
74
134
  declare function useSharedQuery<TArgs extends unknown[], TData>(query: SharedQuery<TArgs, TData>, args?: TArgs): UseQueryResult<TData>;
75
135
 
76
- export { type FetchFn, type PersistTo, type QueryStateValue, SharedQuery, SharedQueryDefaults, type SharedQueryOptions, type SharedQueryState, type UseQueryResult, sharedQuery, useSharedQuery };
136
+ export { type PersistTo, type QueryFn, type QueryStateValue, SharedQuery, SharedQueryDefaults, type SharedQueryOptions, type SharedQueryState, type UseQueryResult, sharedQuery, useSharedQuery };
package/sharedQuery.d.ts CHANGED
@@ -1,14 +1,35 @@
1
1
  import { SharedState, SharedStateOptions } from "./sharedState";
2
- export type FetchFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
2
+ export type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
3
3
  export type QueryStateValue<TData> = {
4
+ /**
5
+ * When this record was last updated. Used for LRU determination. If you want
6
+ * the data age, please use `dataUpdatedMs` instead.
7
+ */
8
+ lastUpdatedMs: number;
4
9
  loading: boolean;
5
10
  data?: TData;
6
11
  dataUpdatedMs?: number;
7
- error?: Error;
12
+ /**
13
+ * Use unknown type for error because we can't assume what type of variable
14
+ * will be thrown from the query function. We also don't want to add this
15
+ * type to QueryStateValue<TError> as it adds complexity to the type.
16
+ */
17
+ error?: unknown;
8
18
  errorUpdatedMs?: number;
9
19
  };
10
20
  export type UseQueryResult<TData> = QueryStateValue<TData> & {
11
- refetch: () => Promise<TData>;
21
+ /**
22
+ * Cancels the inflight query (if any). Returns true if canceled.
23
+ */
24
+ abortCurrentQuery: (reason?: unknown) => boolean;
25
+ /**
26
+ * Manually refresh the data by doing a new query.
27
+ */
28
+ refetch: () => void;
29
+ /**
30
+ * If you got the data from somewhere else (e.g. an in-memory data update),
31
+ * just set it directly.
32
+ */
12
33
  setData: (data: TData, dataUpdatedMs?: number) => void;
13
34
  deleteData: () => void;
14
35
  };
@@ -20,28 +41,49 @@ export type PersistTo = "localStorage" | "indexedDb";
20
41
  export type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
21
42
  export type SharedQueryOptions<TArgs extends unknown[], TData> = {
22
43
  queryName: string;
23
- queryFn: FetchFn<TArgs, TData>;
44
+ queryFn: QueryFn<TArgs, TData>;
24
45
  staleMs?: number;
25
46
  expiryMs?: number;
26
47
  refetchOnStale?: boolean;
27
48
  persistTo?: PersistTo;
49
+ maxSize?: number;
50
+ maxBytes?: number;
51
+ log?: (...args: unknown[]) => void;
28
52
  } & SharedStateOptions<SharedQueryState<TData>>;
29
53
  /** Users can override these values globally. */
30
54
  export declare const SharedQueryDefaults: {
31
55
  staleMs: number;
32
56
  expiryMs: number;
57
+ refetchOnStale: boolean;
33
58
  persistTo: PersistTo | undefined;
59
+ maxSize: number;
60
+ maxBytes: number;
61
+ log: (...args: unknown[]) => void;
34
62
  };
35
63
  export declare class SharedQuery<TArgs extends unknown[], TData> {
36
- private readonly inflightPromises;
64
+ private readonly inflightQueries;
37
65
  readonly queryState: SharedState<SharedQueryState<TData>>;
38
66
  readonly queryName: string;
39
67
  private readonly queryFn;
40
68
  readonly expiryMs: number;
41
69
  readonly staleMs: number;
42
70
  readonly refetchOnStale: boolean;
71
+ readonly maxSize: number;
72
+ readonly maxBytes: number;
73
+ readonly log: (...args: unknown[]) => void;
43
74
  constructor(options: SharedQueryOptions<TArgs, TData>);
75
+ /** Returns a key to identify requests based on the given args. */
44
76
  getQueryKey(args: TArgs): string;
77
+ /**
78
+ * Get the AbortController to abort the inflight query (if any).
79
+ *
80
+ * Note that once you get the controller, you can also get the signal easily
81
+ * using `controller.signal`. So if you need both the controller and the
82
+ * signal, just use this function.
83
+ */
84
+ getAbortController(queryKey: string): AbortController | null;
85
+ /** Get the AbortSignal to check for query abortions. */
86
+ getAbortSignal(queryKey: string): AbortSignal | null;
45
87
  private isStale;
46
88
  private isExpired;
47
89
  getCachedOrFetch(...args: TArgs): Promise<TData>;
@@ -52,9 +94,14 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
52
94
  /** Do a new fetch even when the data is already cached. */
53
95
  updateFromSource(...args: TArgs): Promise<TData>;
54
96
  /** Delete the cached data for one set of arguments. */
55
- deleteData(args: TArgs): void;
97
+ deleteData(queryKey: string): void;
56
98
  /** Delete all currently cached data & all inflight promises. */
57
99
  clear(): void;
100
+ /**
101
+ * Discard the oldest entries if the size exceeds the limit. The last
102
+ * remaining record cannot be deleted no matter what the limit is.
103
+ */
104
+ enforceSizeLimit(): void;
58
105
  }
59
106
  /**
60
107
  * Create a reusable shared query object that can be used to auto de-duplicate
@@ -62,11 +109,24 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
62
109
  *
63
110
  * Each `queryName` must be unique, and can only be declared once, most often
64
111
  * in the top level scope. The queryName is used for logging only.
112
+ *
113
+ * Example:
114
+ * ```
115
+ * export const usersQuery = sharedQuery({
116
+ * queryName: "users",
117
+ * queryFn: listUsers,
118
+ * persistTo: "indexedDb",
119
+ * staleMs: MS_PER_DAY,
120
+ * });
121
+ * ```
65
122
  */
66
123
  export declare function sharedQuery<TArgs extends unknown[], TData>(options: SharedQueryOptions<TArgs, TData>): SharedQuery<TArgs, TData>;
67
124
  /**
68
125
  * React hook to make use of a shared query inside any React component.
69
126
  *
70
- * const users = useSharedQuery(usersQuery, ["123"]);
127
+ * Example:
128
+ * ```
129
+ * const users = useSharedQuery(usersQuery, ["123"]);
130
+ * ```
71
131
  */
72
132
  export declare function useSharedQuery<TArgs extends unknown[], TData>(query: SharedQuery<TArgs, TData>, args?: TArgs): UseQueryResult<TData>;