@ainias42/tanstack-ids 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.
Files changed (88) hide show
  1. package/.ctirc +112 -0
  2. package/.eslintrc.cjs +3 -0
  3. package/.prettierrc +1 -0
  4. package/bin/release.sh +3 -0
  5. package/bin/updatePackages.sh +3 -0
  6. package/dist/NotFoundError.d.ts +3 -0
  7. package/dist/NotFoundError.js +7 -0
  8. package/dist/NotFoundError.js.map +1 -0
  9. package/dist/QueryContext/QueriesContext.d.ts +1 -0
  10. package/dist/QueryContext/QueriesContext.js +3 -0
  11. package/dist/QueryContext/QueriesContext.js.map +1 -0
  12. package/dist/QueryContext/QueriesContextProvider.d.ts +6 -0
  13. package/dist/QueryContext/QueriesContextProvider.js +16 -0
  14. package/dist/QueryContext/QueriesContextProvider.js.map +1 -0
  15. package/dist/QueryContext/SubQueriesContext.d.ts +6 -0
  16. package/dist/QueryContext/SubQueriesContext.js +24 -0
  17. package/dist/QueryContext/SubQueriesContext.js.map +1 -0
  18. package/dist/QueryContext/useGenerateQueriesContextId.d.ts +1 -0
  19. package/dist/QueryContext/useGenerateQueriesContextId.js +7 -0
  20. package/dist/QueryContext/useGenerateQueriesContextId.js.map +1 -0
  21. package/dist/QueryContext/useQueriesError.d.ts +1 -0
  22. package/dist/QueryContext/useQueriesError.js +8 -0
  23. package/dist/QueryContext/useQueriesError.js.map +1 -0
  24. package/dist/QueryContext/useQueriesIsFetching.d.ts +1 -0
  25. package/dist/QueryContext/useQueriesIsFetching.js +8 -0
  26. package/dist/QueryContext/useQueriesIsFetching.js.map +1 -0
  27. package/dist/QueryContext/useQueriesReload.d.ts +1 -0
  28. package/dist/QueryContext/useQueriesReload.js +13 -0
  29. package/dist/QueryContext/useQueriesReload.js.map +1 -0
  30. package/dist/QueryContext/useQueriesStore.d.ts +47 -0
  31. package/dist/QueryContext/useQueriesStore.js +117 -0
  32. package/dist/QueryContext/useQueriesStore.js.map +1 -0
  33. package/dist/QueryContext/useQuerySubscribeToReload.d.ts +1 -0
  34. package/dist/QueryContext/useQuerySubscribeToReload.js +21 -0
  35. package/dist/QueryContext/useQuerySubscribeToReload.js.map +1 -0
  36. package/dist/QueryContext/useScopedQueriesIsFetching.d.ts +4 -0
  37. package/dist/QueryContext/useScopedQueriesIsFetching.js +8 -0
  38. package/dist/QueryContext/useScopedQueriesIsFetching.js.map +1 -0
  39. package/dist/generateIdsQuery.d.ts +215 -0
  40. package/dist/generateIdsQuery.js +154 -0
  41. package/dist/generateIdsQuery.js.map +1 -0
  42. package/dist/generateNestedIdQuery.d.ts +215 -0
  43. package/dist/generateNestedIdQuery.js +25 -0
  44. package/dist/generateNestedIdQuery.js.map +1 -0
  45. package/dist/generateQuery.d.ts +16 -0
  46. package/dist/generateQuery.js +20 -0
  47. package/dist/generateQuery.js.map +1 -0
  48. package/dist/hoistReactQueryKitAttributes.d.ts +2 -0
  49. package/dist/hoistReactQueryKitAttributes.js +9 -0
  50. package/dist/hoistReactQueryKitAttributes.js.map +1 -0
  51. package/dist/hooks/useArrayData.d.ts +6 -0
  52. package/dist/hooks/useArrayData.js +6 -0
  53. package/dist/hooks/useArrayData.js.map +1 -0
  54. package/dist/hooks/useIndexedData.d.ts +4 -0
  55. package/dist/hooks/useIndexedData.js +9 -0
  56. package/dist/hooks/useIndexedData.js.map +1 -0
  57. package/dist/index.d.ts +19 -0
  58. package/dist/index.js +20 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/noop.d.ts +1 -0
  61. package/dist/noop.js +3 -0
  62. package/dist/noop.js.map +1 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -0
  64. package/dist/types/DeepPartial.d.ts +3 -0
  65. package/dist/types/DeepPartial.js +2 -0
  66. package/dist/types/DeepPartial.js.map +1 -0
  67. package/package.json +36 -0
  68. package/src/NotFoundError.ts +6 -0
  69. package/src/QueryContext/QueriesContext.ts +3 -0
  70. package/src/QueryContext/QueriesContextProvider.tsx +28 -0
  71. package/src/QueryContext/SubQueriesContext.tsx +38 -0
  72. package/src/QueryContext/useGenerateQueriesContextId.ts +7 -0
  73. package/src/QueryContext/useQueriesError.ts +8 -0
  74. package/src/QueryContext/useQueriesIsFetching.ts +8 -0
  75. package/src/QueryContext/useQueriesReload.ts +14 -0
  76. package/src/QueryContext/useQueriesStore.ts +150 -0
  77. package/src/QueryContext/useQuerySubscribeToReload.ts +23 -0
  78. package/src/QueryContext/useScopedQueriesIsFetching.ts +9 -0
  79. package/src/generateIdsQuery.ts +286 -0
  80. package/src/generateNestedIdQuery.ts +38 -0
  81. package/src/generateQuery.ts +59 -0
  82. package/src/hoistReactQueryKitAttributes.ts +14 -0
  83. package/src/hooks/useArrayData.ts +19 -0
  84. package/src/hooks/useIndexedData.ts +24 -0
  85. package/src/index.ts +19 -0
  86. package/src/noop.ts +2 -0
  87. package/src/types/DeepPartial.ts +5 -0
  88. package/tsconfig.json +35 -0
@@ -0,0 +1,150 @@
1
+ import { ObjectHelper } from '@ainias42/js-helper';
2
+ import { create } from 'zustand';
3
+ import { immer } from 'zustand/middleware/immer';
4
+ import { subscribeWithSelector } from 'zustand/middleware';
5
+ import type { QueryClient } from '@tanstack/react-query';
6
+ import type { URecord } from '@ainias42/js-helper';
7
+
8
+ type QueryItem = {
9
+ keys: string[];
10
+ isFetching: boolean;
11
+ error: any;
12
+ };
13
+
14
+ function initialState() {
15
+ return {
16
+ lastContextId: 0,
17
+ contexts: {} as URecord<
18
+ number,
19
+ {
20
+ lastId: number;
21
+ items: URecord<number, QueryItem>;
22
+ }
23
+ >,
24
+ subContexts: {} as URecord<number, number[]>
25
+ };
26
+ }
27
+
28
+ export type QueriesStoreState = ReturnType<typeof actionsGenerator> & ReturnType<typeof initialState>;
29
+ type SetState = (
30
+ newState: Partial<QueriesStoreState> | QueriesStoreState | ((state: QueriesStoreState) => QueriesStoreState | void),
31
+ replace?: boolean
32
+ ) => void;
33
+ type GetState = () => Readonly<QueriesStoreState>;
34
+ const actionsGenerator = (set: SetState, get: GetState) => ({
35
+ get,
36
+ clear() {
37
+ set({ ...initialState(), ...actionsGenerator(set, get) }, true);
38
+ },
39
+ generateContextId() {
40
+ let id = 0;
41
+ set(state => {
42
+ state.lastContextId++;
43
+ state.contexts[state.lastContextId] = {
44
+ lastId: 0,
45
+ items: {}
46
+ };
47
+ id = state.lastContextId;
48
+ });
49
+ return id;
50
+ },
51
+ setItem(contextId: number, itemId: number | undefined, item: QueryItem) {
52
+ let id = itemId;
53
+ set(state => {
54
+ const contextData = state.contexts[contextId];
55
+ if (!contextData) {
56
+ return;
57
+ }
58
+
59
+ if (!id) {
60
+ contextData.lastId++;
61
+ id = contextData.lastId;
62
+ }
63
+ contextData.items[id] = item;
64
+ });
65
+ return id;
66
+ },
67
+ removeItem(contextId: number, itemId: number | undefined) {
68
+ if (!itemId) {
69
+ return;
70
+ }
71
+ set(state => {
72
+ delete state.contexts[contextId]?.items[itemId];
73
+ });
74
+ },
75
+ isFetching(contextId: number): boolean {
76
+ const state = get();
77
+ return (
78
+ ObjectHelper.values(state.contexts[contextId]?.items ?? {}).some(item => item.isFetching) ||
79
+ (state.subContexts[contextId]?.some(id => state.isFetching(id)) ?? false)
80
+ );
81
+ },
82
+ isFetchingByScope(contextId: number, scope: { global: boolean; subContexts: boolean }): boolean {
83
+ const state = get();
84
+ return (
85
+ ((scope.global &&
86
+ ObjectHelper.values(state.contexts[contextId]?.items ?? {}).some(item => item.isFetching)) ||
87
+ (scope.subContexts && state.subContexts[contextId]?.some(id => state.isFetching(id)))) ??
88
+ false
89
+ );
90
+ },
91
+ getError(contextId: number): any {
92
+ const state = get();
93
+
94
+ const error = ObjectHelper.values(state.contexts[contextId]?.items ?? {}).find(item => item.error)?.error;
95
+ if (error) {
96
+ return error;
97
+ }
98
+ const subContextWithError = state.subContexts[contextId]?.find(childId => state.getError(childId));
99
+ if (subContextWithError) {
100
+ return state.getError(subContextWithError);
101
+ }
102
+ return undefined;
103
+ },
104
+ getAllKeys(contextId: number): Set<string> {
105
+ const state = get();
106
+
107
+ const keys = new Set(ObjectHelper.values(state.contexts[contextId]?.items ?? {}).flatMap(item => item.keys));
108
+
109
+ const subContext = state.subContexts[contextId];
110
+ if (subContext) {
111
+ for (const childId of subContext) {
112
+ const subKeys = state.getAllKeys(childId);
113
+ for (const key of subKeys) {
114
+ keys.add(key);
115
+ }
116
+ }
117
+ }
118
+
119
+ return keys;
120
+ },
121
+ reload(queryClient: QueryClient, contextId: number) {
122
+ const state = get();
123
+
124
+ const keys = state.getAllKeys(contextId);
125
+ return queryClient.refetchQueries({
126
+ predicate: query => keys.has(query.queryHash)
127
+ });
128
+ },
129
+ addSubContext(parent: number, child: number) {
130
+ set(state => {
131
+ const newChildren = state.subContexts[parent] ?? [];
132
+ newChildren.push(child);
133
+ state.subContexts[parent] = newChildren;
134
+ });
135
+ },
136
+ removeSubContext(parent: number, child: number) {
137
+ set(state => {
138
+ state.subContexts[parent] = state.subContexts[parent]?.filter(id => id !== child);
139
+ });
140
+ }
141
+ });
142
+
143
+ export const useQueriesStore = create<QueriesStoreState>()(
144
+ subscribeWithSelector(
145
+ immer((set, get) => ({
146
+ ...initialState(),
147
+ ...actionsGenerator(set, get)
148
+ }))
149
+ )
150
+ );
@@ -0,0 +1,23 @@
1
+ import { QueriesContext } from '@/QueryContext/QueriesContext';
2
+ import { useContext, useLayoutEffect, useRef } from 'react';
3
+ import { useQueriesStore } from '@/QueryContext/useQueriesStore';
4
+
5
+ export function useQueriesSubscribeToReload(keys: string[], isFetching: boolean, error: any) {
6
+ const contextId = useContext(QueriesContext);
7
+ const setItem = useQueriesStore(s => s.setItem);
8
+ const removeItem = useQueriesStore(s => s.removeItem);
9
+ const id = useRef<number | undefined>();
10
+
11
+ useLayoutEffect(() => {
12
+ id.current = setItem(contextId, id.current, {
13
+ keys,
14
+ error,
15
+ isFetching
16
+ });
17
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18
+ }, [contextId, error, isFetching, setItem, ...keys]);
19
+
20
+ useLayoutEffect(() => {
21
+ return () => removeItem(contextId, id.current);
22
+ }, [contextId, removeItem]);
23
+ }
@@ -0,0 +1,9 @@
1
+ import { QueriesContext } from '@/QueryContext/QueriesContext';
2
+ import { useContext } from 'react';
3
+ import { useQueriesStore } from '@/QueryContext/useQueriesStore';
4
+
5
+ export function useScopedQueriesIsFetching(scope: { global: boolean; subContexts: boolean }, contextId?: number) {
6
+ const context = useContext(QueriesContext);
7
+
8
+ return useQueriesStore(s => s.isFetchingByScope(contextId ?? context, scope));
9
+ }
@@ -0,0 +1,286 @@
1
+ import { ArrayHelper, JsonHelper } from '@ainias42/js-helper';
2
+ import { NotFoundError } from '@/NotFoundError';
3
+ import { create, windowScheduler } from '@yornaath/batshit';
4
+ import { createInfiniteQuery, createQuery } from 'react-query-kit';
5
+ import { generateQuery } from '@/generateQuery';
6
+ import { hashKey, useQueries } from '@tanstack/react-query';
7
+ import { hoistReactQueryKitAttributes } from '@/hoistReactQueryKitAttributes';
8
+ import { useIndexedData } from '@/hooks/useIndexedData';
9
+ import { useMemo, useRef } from 'react';
10
+ import { useQueriesSubscribeToReload } from '@/QueryContext/useQuerySubscribeToReload';
11
+ import type { CreateInfiniteQueryOptions, CreateQueryOptions } from 'react-query-kit';
12
+ import type { QueriesResults } from '@tanstack/react-query/build/legacy/useQueries';
13
+ import type { QueryClient, QueryFunctionContext, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
14
+ import type { URecord } from '@ainias42/js-helper';
15
+
16
+ export type Unpacked<T> = T extends (infer U)[] ? U : T;
17
+
18
+ type DeepPartial<T> = T extends object
19
+ ? {
20
+ [P in keyof T]?: DeepPartial<T[P]>;
21
+ }
22
+ : T;
23
+
24
+ export function generateIdsQuery<
25
+ IdType extends number | string | symbol,
26
+ Type extends {
27
+ id: IdType;
28
+ }
29
+ >(
30
+ key: string,
31
+ fetcher: (ids: IdType[]) => Promise<Type[]> | Type[],
32
+ additionalQueryOptions?: Omit<CreateQueryOptions<Type, [IdType]>, 'queryKey' | 'fetcher'> & {
33
+ schedulerTime?: number;
34
+ }
35
+ ) {
36
+ const batch = create<URecord<IdType, Type>[], IdType, Type>({
37
+ fetcher: async ids => {
38
+ return [ArrayHelper.arrayToObject(await fetcher(ids), value => value.id)];
39
+ },
40
+ resolver: ([items], id) => {
41
+ const item = items?.[id];
42
+ if (item === undefined) {
43
+ throw new NotFoundError(id);
44
+ }
45
+ return item;
46
+ },
47
+ scheduler: windowScheduler(additionalQueryOptions?.schedulerTime ?? 15)
48
+ });
49
+
50
+ const useQueryHook = createQuery<Type, [IdType]>({
51
+ queryKey: [key],
52
+ fetcher: identifier => batch.fetch(identifier[0]),
53
+ ...additionalQueryOptions
54
+ });
55
+
56
+ function useManyHook(
57
+ ids: IdType[],
58
+ otherAdditionalQueryOptions: Omit<
59
+ UseQueryOptions<Type, unknown, Type, [string, [IdType]]>,
60
+ 'context' | 'fetcher' | 'queryKey'
61
+ > & { reloadWithContext?: boolean } = {}
62
+ ) {
63
+ const queries = ids.map(id => ({
64
+ ...useQueryHook.getOptions([id]),
65
+ ...otherAdditionalQueryOptions
66
+ }));
67
+
68
+ const results = useQueries<Type[], QueriesResults<Type[]>>({
69
+ // @ts-expect-error wrong type in tanstack-query
70
+ queries
71
+ }) as UseQueryResult<Type, Error>[];
72
+
73
+ const oldData = useRef<typeof results>([]);
74
+ const isSame = JsonHelper.deepEqual(oldData.current, results, 1);
75
+
76
+ const returnData = useRef({} as URecord<IdType, Unpacked<typeof results>>);
77
+ if (!isSame) {
78
+ oldData.current = results;
79
+ returnData.current = {} as URecord<IdType, Unpacked<typeof results>>;
80
+ for (const [index, val] of results.entries()) {
81
+ const id = ids[index] as IdType;
82
+ returnData.current[id] = val;
83
+ }
84
+ }
85
+
86
+ const shouldReload = otherAdditionalQueryOptions.reloadWithContext ?? true;
87
+ const keys = shouldReload ? ids.map(id => hashKey(useQueryHook.getKey([id as DeepPartial<IdType>]))) : [];
88
+
89
+ // eslint-disable-next-line react-hooks/exhaustive-deps
90
+ const isFetching = useMemo(() => oldData.current.some(item => item.isFetching), [oldData.current]);
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ const error = useMemo(() => oldData.current.find(item => item.error)?.error, [oldData.current]);
93
+
94
+ useQueriesSubscribeToReload(keys, isFetching, error);
95
+
96
+ return returnData.current;
97
+ }
98
+
99
+ function useOneHook(identifier: IdType) {
100
+ const result = useQueryHook({variables: [identifier]});
101
+ useQueriesSubscribeToReload(
102
+ [hashKey(useQueryHook.getKey([identifier as DeepPartial<IdType>]))],
103
+ result.isFetching,
104
+ result.error
105
+ );
106
+ return result;
107
+ }
108
+
109
+ function generateFilter<
110
+ Func extends (...args: any[]) => Promise<Type[]> | Type[],
111
+ FuncArguments extends Parameters<Func>['length'] extends 0
112
+ ? void
113
+ : Parameters<Func> = Parameters<Func>['length'] extends 0 ? void : Parameters<Func>,
114
+ ResultAsArray extends boolean = false
115
+ >(
116
+ queryClient: QueryClient,
117
+ filterFunc: Func,
118
+ additionalOptions?: Omit<CreateQueryOptions<IdType[], [FuncArguments]>, 'queryKey' | 'fetcher'> & {
119
+ resultAsArray?: ResultAsArray;
120
+ subKey?: string;
121
+ }
122
+ ) {
123
+ const queryHook = generateQuery(
124
+ `${key}-filter${additionalOptions?.subKey ? `-${additionalOptions.subKey}` : ''}`,
125
+ async (filter: FuncArguments) => {
126
+ const result = await filterFunc(filter);
127
+ return result.map(item => {
128
+ queryClient.setQueryData(useQueryHook.getKey([item.id as DeepPartial<IdType>]), item);
129
+ return item.id;
130
+ });
131
+ },
132
+ {
133
+ initialDataUpdatedAt: 0,
134
+ ...additionalOptions,
135
+ initialData: (additionalOptions?.initialData as IdType[]) ?? []
136
+ }
137
+ );
138
+
139
+ type QueryHookArgument = Parameters<typeof queryHook>[0];
140
+ type HookArgs = FuncArguments extends void ? [] : FuncArguments;
141
+
142
+ function useFilter<Args extends HookArgs>(
143
+ ...args: Args
144
+ ): ResultAsArray extends true ? UseQueryResult<Type, Error>[] : URecord<IdType, UseQueryResult<Type, Error>> {
145
+ const result = queryHook({variables: args} as QueryHookArgument);
146
+
147
+ useQueriesSubscribeToReload(
148
+ [hashKey(queryHook.getKey(args as Parameters<(typeof queryHook)['getKey']>[0]))],
149
+ result.isFetching,
150
+ result.error
151
+ );
152
+
153
+ // When staleTime is same as filterStaleTime, then the queries will run at the same time as the filter
154
+ // Because of this, use staleTime: Inifinity to prevent the queries from running again. The result of the
155
+ // filter will be saved inside the queryClient, so the queries will be updated with the new data.
156
+ // Same goes for reloading with context.
157
+ // Other usages of useManyHook are not affected by this.
158
+ const indexedResult = useManyHook(result.data, {staleTime: Infinity, reloadWithContext: false});
159
+ const arrayResult: UseQueryResult<Type, Error>[] = useMemo(
160
+ () => ArrayHelper.noUndefined(result.data.map(id => indexedResult[id])),
161
+ [indexedResult, result.data]
162
+ );
163
+
164
+ if (additionalOptions?.resultAsArray) {
165
+ return arrayResult as ResultAsArray extends true
166
+ ? UseQueryResult<Type, Error>[]
167
+ : URecord<IdType, UseQueryResult<Type, Error>>;
168
+ }
169
+ return indexedResult as ResultAsArray extends true
170
+ ? UseQueryResult<Type, Error>[]
171
+ : URecord<IdType, UseQueryResult<Type, Error>>;
172
+
173
+ }
174
+
175
+ return hoistReactQueryKitAttributes(queryHook, useFilter);
176
+ }
177
+
178
+ function generateInfiniteQuery<PageParams, FuncArguments extends any[]>(
179
+ queryClient: QueryClient,
180
+ {
181
+ queryFn,
182
+ ...options
183
+ }: Omit<
184
+ CreateInfiniteQueryOptions<IdType[], FuncArguments, Error, PageParams>,
185
+ 'getNextPageParam' | 'getPreviousPageParam' | 'queryKey' | 'fetcher'
186
+ > & {
187
+ queryFn: (
188
+ pageParams: PageParams,
189
+ args: FuncArguments,
190
+ context: QueryFunctionContext<[string, FuncArguments], PageParams>
191
+ ) => Promise<Type[]> | Type[];
192
+ getNextPageParam: (options: {
193
+ getLastPage: () => Type[];
194
+ getAllPages: () => Type[][];
195
+ lastPageParam: PageParams;
196
+ allPageParams: PageParams[];
197
+ lastPageLength: number;
198
+ }) => PageParams | undefined;
199
+ getPreviousPageParam?: (options: {
200
+ getFirstPage: () => Type[];
201
+ getAllPages: () => Type[][];
202
+ firstPageParam: PageParams;
203
+ allPageParams: PageParams[];
204
+ firstPageLength: number;
205
+ }) => PageParams | undefined;
206
+ }
207
+ ) {
208
+ function findEntity(id: IdType) {
209
+ return queryClient.getQueryData(useQueryHook.getKey([id as DeepPartial<IdType>])) as Type | undefined;
210
+ }
211
+
212
+ const {getPreviousPageParam} = options;
213
+ const infiniteQueryHook = createInfiniteQuery<IdType[], FuncArguments, Error, PageParams>({
214
+ ...options,
215
+ queryKey: [`${key}-infinite-query`],
216
+ fetcher: async (_, ctx) => {
217
+ const context = ctx as typeof ctx & { pageParam: PageParams };
218
+ const result = await queryFn(
219
+ context.pageParam,
220
+ context.queryKey[1] as FuncArguments,
221
+ context as QueryFunctionContext<[string, FuncArguments], PageParams>
222
+ );
223
+ return result.map(item => {
224
+ queryClient.setQueryData(useQueryHook.getKey([item.id as DeepPartial<IdType>]), item);
225
+ return item.id;
226
+ });
227
+ },
228
+ getNextPageParam: (lastPage: IdType[], allPages: IdType[][], lastPageParam, allPageParams) => {
229
+ return options.getNextPageParam({
230
+ getLastPage: () => ArrayHelper.noUndefined(lastPage.map(findEntity)),
231
+ getAllPages: () => allPages.map(page => ArrayHelper.noUndefined(page.map(findEntity))),
232
+ lastPageParam,
233
+ allPageParams,
234
+ lastPageLength: lastPage.length
235
+ });
236
+ },
237
+ getPreviousPageParam: !getPreviousPageParam
238
+ ? undefined
239
+ : (firstPage, allPages, firstPageParam, allPageParams) => {
240
+ return getPreviousPageParam({
241
+ getFirstPage: () => ArrayHelper.noUndefined(firstPage.map(findEntity)),
242
+ getAllPages: () => allPages.map(page => ArrayHelper.noUndefined(page.map(findEntity))),
243
+ firstPageParam,
244
+ allPageParams,
245
+ firstPageLength: firstPage.length
246
+ });
247
+ }
248
+ });
249
+
250
+ function useInfiniteQuery(...args: FuncArguments) {
251
+ const result = infiniteQueryHook({variables: args});
252
+ useQueriesSubscribeToReload(
253
+ [hashKey(infiniteQueryHook.getKey(args as Parameters<(typeof infiniteQueryHook)['getKey']>[0]))],
254
+ result.isFetching,
255
+ result.error
256
+ );
257
+ const ids = useMemo(() => result.data?.pages.flat() ?? [], [result.data?.pages]);
258
+
259
+ // When staleTime is same as filterStaleTime, then the queries will run at the same time as the filter
260
+ // Because of this, use staleTime: Inifinity to prevent the queries from running again. The result of the
261
+ // filter will be saved inside the queryClient, so the queries will be updated with the new data.
262
+ // Same goes for reloading with context.
263
+ // Other usages of useManyHook are not affected by this.
264
+ const manyData = useManyHook(ids, {staleTime: Infinity, reloadWithContext: false});
265
+ const indexedData = useIndexedData(manyData);
266
+
267
+ const pages = useMemo(() => {
268
+ return result.data
269
+ ? result.data.pages.map(page => ArrayHelper.noUndefined(page.map(id => indexedData[id])))
270
+ : [];
271
+ }, [indexedData, result.data]);
272
+
273
+ return {
274
+ ...result,
275
+ data: !result.data ? undefined : {...result.data, pages}
276
+ };
277
+ }
278
+
279
+ // @ts-expect-error type is okay
280
+ return hoistReactQueryKitAttributes(infiniteQueryHook, useInfiniteQuery);
281
+ }
282
+
283
+ const useMany = hoistReactQueryKitAttributes(useQueryHook, useManyHook);
284
+ const useOne = hoistReactQueryKitAttributes(useQueryHook, useOneHook);
285
+ return {useMany, useOne, generateFilter, generateInfiniteQuery};
286
+ }
@@ -0,0 +1,38 @@
1
+ import { type QueryClient, hashKey } from '@tanstack/react-query';
2
+ import { generateIdsQuery } from '@/generateIdsQuery';
3
+ import { noop } from '@/noop';
4
+ import { useMemo } from 'react';
5
+ import type { DeepPartial } from '@/types/DeepPartial';
6
+
7
+ export function generateNestedIdQuery<
8
+ IdType extends number | string | symbol,
9
+ Type extends {
10
+ id: IdType;
11
+ },
12
+ Args extends any[]
13
+ >(key: string, fetcherFactory: (...args: Args) => (ids: IdType[]) => Promise<Type[]> | Type[]) {
14
+ const factory = (...args: Args) => {
15
+ // eslint-disable-next-line react-hooks/rules-of-hooks
16
+ return useMemo(() => {
17
+ const keyWithArgs = `${key}-${hashKey(args)}`;
18
+ return generateIdsQuery(keyWithArgs, fetcherFactory(...args));
19
+ }, [args]);
20
+ };
21
+
22
+ factory.getFilterKey = (args: Args, variables?: any[]) => {
23
+ const keyWithArgs = `${key}-${hashKey(args)}`;
24
+ return generateIdsQuery(keyWithArgs, fetcherFactory(...args))
25
+ .generateFilter(undefined as unknown as QueryClient, noop as (..._: any[]) => Type[])
26
+ .getKey([variables]);
27
+ };
28
+
29
+ factory.getKey = <Variables extends [(DeepPartial<IdType> | undefined)?] | undefined = undefined>(
30
+ args: Args,
31
+ variables?: Variables
32
+ ) => {
33
+ const keyWithArgs = `${key}-${hashKey(args)}`;
34
+ return generateIdsQuery(keyWithArgs, fetcherFactory(...args)).useOne.getKey(variables);
35
+ };
36
+
37
+ return factory;
38
+ }
@@ -0,0 +1,59 @@
1
+ import { createQuery } from 'react-query-kit';
2
+ import { hashKey } from '@tanstack/react-query';
3
+ import { hoistReactQueryKitAttributes } from '@/hoistReactQueryKitAttributes';
4
+ import { useQueriesSubscribeToReload } from '@/QueryContext/useQuerySubscribeToReload';
5
+ import type { CreateQueryOptions, QueryHookResult } from 'react-query-kit';
6
+
7
+ export function generateQuery<
8
+ Func extends (...args: any[]) => Promise<any> | any,
9
+ FuncArguments extends Parameters<Func>['length'] extends 0
10
+ ? void
11
+ : Parameters<Func> = Parameters<Func>['length'] extends 0 ? void : Parameters<Func>,
12
+ FuncReturnType extends Awaited<ReturnType<Func>> = Awaited<ReturnType<Func>>,
13
+ AdditionalOptionsType extends Omit<
14
+ CreateQueryOptions<FuncReturnType, FuncArguments>,
15
+ 'queryKey' | 'fetcher'
16
+ > = Omit<CreateQueryOptions<FuncReturnType, FuncArguments>, 'queryKey' | 'fetcher'>
17
+ >(primaryKey: string, queryFn: Func, additionalOptions?: AdditionalOptionsType) {
18
+ const queryHook = createQuery<FuncReturnType, FuncArguments>({
19
+ queryKey: [primaryKey],
20
+ fetcher: variables => {
21
+ if (Array.isArray(variables)) {
22
+ return queryFn(...variables);
23
+ }
24
+ return queryFn();
25
+
26
+ },
27
+ initialDataUpdatedAt: 0,
28
+ ...additionalOptions
29
+ });
30
+
31
+ type QueryHookArgument = Parameters<typeof queryHook>[0];
32
+ type HookArgs = FuncArguments extends void ? [] | [QueryHookArgument] : [QueryHookArgument];
33
+
34
+ function useHook<Arg extends HookArgs>(...options: Arg) {
35
+ type DataReturnType = Arg extends [
36
+ {
37
+ initialData: FuncReturnType | (() => FuncReturnType);
38
+ }
39
+ ]
40
+ ? FuncReturnType
41
+ : AdditionalOptionsType extends { initialData: FuncReturnType | (() => FuncReturnType) }
42
+ ? FuncReturnType
43
+ : FuncReturnType | undefined;
44
+
45
+ const result = queryHook(options[0] as QueryHookArgument);
46
+ useQueriesSubscribeToReload(
47
+ [hashKey(queryHook.getKey(options[0]?.variables as Parameters<(typeof queryHook)['getKey']>[0]))],
48
+ result.isFetching,
49
+ result.error
50
+ );
51
+
52
+ type Return = Arg extends [{ select: (...arg: any[]) => any }] ? ReturnType<Arg[0]['select']> : DataReturnType;
53
+ return result as QueryHookResult<Return, Error> & {
54
+ data: Return;
55
+ };
56
+ }
57
+
58
+ return hoistReactQueryKitAttributes(queryHook, useHook);
59
+ }
@@ -0,0 +1,14 @@
1
+ import type { ExposeMethods } from 'react-query-kit/build/lib/types';
2
+
3
+ export function hoistReactQueryKitAttributes<ReturnType, HookArgs, ToFunc extends (...args: any) => any, Error>(
4
+ from: ExposeMethods<ReturnType, HookArgs, Error>,
5
+ to: ToFunc
6
+ ) {
7
+ const partialHook: Partial<ExposeMethods<ReturnType, HookArgs, Error>> & ToFunc = to;
8
+ const hoistedOptions = ['fetcher', 'getKey', 'queryKeyHashFn', 'getOptions'] as const;
9
+ for (const name of hoistedOptions) {
10
+ partialHook[name as 'getKey'] = from[name as 'getKey'];
11
+ }
12
+
13
+ return partialHook as ExposeMethods<ReturnType, HookArgs, Error> & ToFunc;
14
+ }
@@ -0,0 +1,19 @@
1
+ import { ArrayHelper, ObjectHelper } from '@ainias42/js-helper';
2
+ import { useMemo } from 'react';
3
+ import type { URecord } from '@ainias42/js-helper';
4
+
5
+ export function useArrayData<DataType>(
6
+ result:
7
+ | {
8
+ data: DataType;
9
+ }[]
10
+ | URecord<any, { data: DataType | undefined }>
11
+ ): Exclude<DataType, undefined>[] {
12
+ return useMemo(
13
+ () =>
14
+ ArrayHelper.noUndefined(
15
+ (Array.isArray(result) ? result : ObjectHelper.values(result)).map(data => data.data)
16
+ ),
17
+ [result]
18
+ );
19
+ }
@@ -0,0 +1,24 @@
1
+ import { ObjectHelper } from '@ainias42/js-helper';
2
+ import { useMemo } from 'react';
3
+ import type { URecord } from '@ainias42/js-helper';
4
+
5
+ export function useIndexedData<DataType, IndexType extends number | string | symbol>(
6
+ result: URecord<
7
+ IndexType,
8
+ {
9
+ data: DataType | undefined;
10
+ }
11
+ >
12
+ ) {
13
+ return useMemo(
14
+ () =>
15
+ ObjectHelper.entries(result).reduce(
16
+ (acc, [key, data]) => {
17
+ acc[key] = data.data;
18
+ return acc;
19
+ },
20
+ {} as URecord<IndexType, DataType>
21
+ ),
22
+ [result]
23
+ );
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export * from './generateIdsQuery';
2
+ export * from './generateNestedIdQuery';
3
+ export * from './generateQuery';
4
+ export * from './hoistReactQueryKitAttributes';
5
+ export * from './noop';
6
+ export * from './NotFoundError';
7
+ export * from './hooks/useArrayData';
8
+ export * from './hooks/useIndexedData';
9
+ export * from './QueryContext/QueriesContext';
10
+ export * from './QueryContext/QueriesContextProvider';
11
+ export * from './QueryContext/SubQueriesContext';
12
+ export * from './QueryContext/useGenerateQueriesContextId';
13
+ export * from './QueryContext/useQueriesError';
14
+ export * from './QueryContext/useQueriesIsFetching';
15
+ export * from './QueryContext/useQueriesReload';
16
+ export * from './QueryContext/useQueriesStore';
17
+ export * from './QueryContext/useQuerySubscribeToReload';
18
+ export * from './QueryContext/useScopedQueriesIsFetching';
19
+ export * from './types/DeepPartial';
package/src/noop.ts ADDED
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2
+ export function noop() {}
@@ -0,0 +1,5 @@
1
+ export type DeepPartial<T> = T extends object
2
+ ? {
3
+ [P in keyof T]?: DeepPartial<T[P]>;
4
+ }
5
+ : T;