@ventlio/tanstack-query 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { QueryKey, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query';
2
- import { startTransition, useEffect, useMemo, useState } from 'react';
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { useEnvironmentVariables } from '../config';
4
4
 
5
5
  import { useStore } from '@tanstack/react-store';
@@ -33,7 +33,6 @@ export const useGetRequest = <TResponse extends Record<string, any>>({
33
33
  };
34
34
  } & DefaultRequestOptions) => {
35
35
  const [requestPath, setRequestPath] = useState<string>(path);
36
- const [options, setOptions] = useState<any>(queryOptions);
37
36
  const [page, setPage] = useState<number>(1);
38
37
 
39
38
  const { API_URL, TIMEOUT } = useEnvironmentVariables();
@@ -49,14 +48,9 @@ export const useGetRequest = <TResponse extends Record<string, any>>({
49
48
  return { ...providerHeaders, ...storeHeaders };
50
49
  }, [storeHeaders, headerProvider]);
51
50
 
52
- const [requestPayload, setRequestPayload] = useState<Record<any, any>>();
53
-
54
51
  const isFutureQueriesPaused = usePauseFutureRequests((state) => state.isFutureQueriesPaused);
55
52
 
56
- let queryClient = useQueryClient();
57
-
58
- // eslint-disable-next-line react-hooks/exhaustive-deps
59
- queryClient = useMemo(() => queryClient, []);
53
+ const queryClient = useQueryClient();
60
54
 
61
55
  // Merge global and local pagination config
62
56
  const pagination = useMemo(
@@ -67,73 +61,74 @@ export const useGetRequest = <TResponse extends Record<string, any>>({
67
61
  [globalPaginationConfig, paginationConfig]
68
62
  );
69
63
 
70
- const sendRequest = async (
71
- res: (
72
- value: IRequestError | IRequestSuccess<TResponse> | PromiseLike<IRequestError | IRequestSuccess<TResponse>>
73
- ) => void,
74
- rej: (reason?: any) => void,
75
- queryKey: QueryKey
76
- ) => {
77
- const [url] = queryKey;
78
- const requestUrl = (url ?? requestPath) as string;
79
-
80
- const requestOptions = {
81
- path: requestUrl,
82
- headers: { ...globalHeaders, ...headers },
83
- baseURL: baseUrl ?? API_URL,
84
- timeout: TIMEOUT,
85
- };
86
-
87
- // Create the final handler that makes the actual request
88
- const finalHandler: MiddlewareNext<TResponse> = async (options) => {
89
- const finalOptions = options ? { ...requestOptions, ...options } : requestOptions;
90
- return await makeRequest<TResponse>(finalOptions);
91
- };
92
-
93
- let getResponse: IRequestError | IRequestSuccess<TResponse>;
94
-
95
- // If middleware is available, execute the middleware chain
96
- if (middleware && Array.isArray(middleware) && middleware.length > 0) {
97
- const context: MiddlewareContext<TResponse> = {
98
- baseUrl: baseUrl ?? API_URL,
64
+ /**
65
+ * Core request function that makes the actual HTTP request
66
+ */
67
+ const executeRequest = useCallback(
68
+ async (requestUrl: string): Promise<IRequestSuccess<TResponse>> => {
69
+ const requestOptions = {
99
70
  path: requestUrl,
100
- options: requestOptions,
71
+ headers: { ...globalHeaders, ...headers },
72
+ baseURL: baseUrl ?? API_URL,
73
+ timeout: TIMEOUT,
101
74
  };
102
75
 
103
- getResponse = await executeMiddlewareChain<TResponse>(middleware, context, finalHandler);
104
- } else {
105
- // Otherwise, just make the request directly
106
- getResponse = await makeRequest<TResponse>(requestOptions);
107
- }
76
+ // Create the final handler that makes the actual request
77
+ const finalHandler: MiddlewareNext<TResponse> = async (options) => {
78
+ const finalOptions = options ? { ...requestOptions, ...options } : requestOptions;
79
+ return await makeRequest<TResponse>(finalOptions);
80
+ };
108
81
 
109
- if (getResponse.status) {
110
- res(getResponse as IRequestSuccess<TResponse>);
111
- } else {
112
- rej(getResponse);
113
- }
114
- };
82
+ let getResponse: IRequestError | IRequestSuccess<TResponse>;
83
+
84
+ // If middleware is available, execute the middleware chain
85
+ if (middleware && Array.isArray(middleware) && middleware.length > 0) {
86
+ const context: MiddlewareContext<TResponse> = {
87
+ baseUrl: baseUrl ?? API_URL,
88
+ path: requestUrl,
89
+ options: requestOptions,
90
+ };
91
+
92
+ getResponse = await executeMiddlewareChain<TResponse>(middleware, context, finalHandler);
93
+ } else {
94
+ // Otherwise, just make the request directly
95
+ getResponse = await makeRequest<TResponse>(requestOptions);
96
+ }
97
+
98
+ if (getResponse.status) {
99
+ return getResponse as IRequestSuccess<TResponse>;
100
+ } else {
101
+ throw getResponse;
102
+ }
103
+ },
104
+ [globalHeaders, headers, baseUrl, API_URL, TIMEOUT, middleware]
105
+ );
115
106
 
116
- const query = useQuery<any, any, IRequestSuccess<TResponse>>({
117
- queryKey: [requestPath, {}],
118
- queryFn: ({ queryKey }) =>
119
- new Promise<IRequestSuccess<TResponse> | IRequestError>((res, rej) => sendRequest(res, rej, queryKey)),
120
- enabled: load && !isFutureQueriesPaused,
121
- ...options,
107
+ // The declarative query - only runs when load is true
108
+ const query = useQuery({
109
+ queryKey: [requestPath, {}] as const,
110
+ queryFn: async ({ queryKey }) => {
111
+ const [url] = queryKey;
112
+ return executeRequest(url);
113
+ },
114
+ // Only enable when load is explicitly true AND queries aren't paused
115
+ enabled: load === true && !isFutureQueriesPaused,
116
+ ...queryOptions,
122
117
  });
123
118
 
119
+ // Update request path when prop changes
124
120
  useEffect(() => {
125
- if (path) {
121
+ if (path && path !== requestPath) {
126
122
  setRequestPath(path);
127
123
  }
128
- }, [path]);
124
+ }, [path, requestPath]);
129
125
 
126
+ // Track query key for external reference
130
127
  useEffect(() => {
131
128
  if (keyTracker) {
132
- // set expiration time for the tracker
133
129
  queryClient.setQueryDefaults([keyTracker], {
134
130
  staleTime: Infinity,
135
131
  });
136
-
137
132
  queryClient.setQueryData([keyTracker], [requestPath, {}]);
138
133
  }
139
134
  }, [keyTracker, requestPath, queryClient, queryOptions?.staleTime]);
@@ -141,142 +136,149 @@ export const useGetRequest = <TResponse extends Record<string, any>>({
141
136
  /**
142
137
  * Extract pagination data from response using configured extractor
143
138
  */
144
- const getPaginationData = (response: IRequestSuccess<TResponse>): IPagination | undefined => {
145
- // Use the configured pagination extractor or fall back to default
146
- const extractPagination =
147
- pagination.extractPagination ||
148
- ((res) => {
149
- if ('pagination' in res.data) {
150
- return res.data.pagination as IPagination;
151
- }
152
- return undefined;
153
- });
139
+ const getPaginationData = useCallback(
140
+ (response: IRequestSuccess<TResponse>): IPagination | undefined => {
141
+ const extractPagination =
142
+ pagination.extractPagination ||
143
+ ((res) => {
144
+ if ('pagination' in res.data) {
145
+ return res.data.pagination as IPagination;
146
+ }
147
+ return undefined;
148
+ });
149
+
150
+ return extractPagination(response);
151
+ },
152
+ [pagination.extractPagination]
153
+ );
154
154
 
155
- return extractPagination(response);
156
- };
155
+ /**
156
+ * Construct a pagination URL using the configured builder
157
+ */
158
+ const constructPaginationLink = useCallback(
159
+ (link: string, pageNumber: number) => {
160
+ const buildPaginationUrl =
161
+ pagination.buildPaginationUrl ||
162
+ ((url, targetPage) => {
163
+ const [pathname, queryString] = url.split('?');
164
+ const queryParams = new URLSearchParams(queryString || '');
165
+ const pageParamName = pagination.pageParamName || 'page';
166
+
167
+ queryParams.set(pageParamName, String(targetPage));
168
+ return pathname + '?' + queryParams.toString();
169
+ });
170
+
171
+ return buildPaginationUrl(link, pageNumber);
172
+ },
173
+ [pagination.buildPaginationUrl, pagination.pageParamName]
174
+ );
157
175
 
158
176
  /**
159
177
  * Navigate to the next page if available
160
178
  */
161
- const nextPage = () => {
162
- // The linter thinks query.data is always falsy, but we know it can be defined after a successful query
163
- // Let's restructure to avoid the conditional
164
- const paginationData = (query as any).data && getPaginationData(query.data);
179
+ const nextPage = useCallback(() => {
180
+ const data = query.data as IRequestSuccess<TResponse> | undefined;
181
+ if (!data) return;
182
+
183
+ const paginationData = getPaginationData(data);
165
184
  if (!paginationData) return;
166
185
 
167
186
  if (
168
187
  paginationData.next_page !== paginationData.current_page &&
169
188
  paginationData.next_page > paginationData.current_page
170
189
  ) {
171
- setRequestPath(constructPaginationLink(requestPath, paginationData.next_page));
190
+ const newPath = constructPaginationLink(requestPath, paginationData.next_page);
191
+ setRequestPath(newPath);
192
+ setPage(paginationData.next_page);
172
193
  }
173
- };
194
+ }, [query.data, getPaginationData, constructPaginationLink, requestPath]);
174
195
 
175
196
  /**
176
197
  * Navigate to the previous page if available
177
198
  */
178
- const prevPage = () => {
179
- // The linter thinks query.data is always falsy, but we know it can be defined after a successful query
180
- // Let's restructure to avoid the conditional
181
- const paginationData = (query as any).data && getPaginationData(query.data);
199
+ const prevPage = useCallback(() => {
200
+ const data = query.data as IRequestSuccess<TResponse> | undefined;
201
+ if (!data) return;
202
+
203
+ const paginationData = getPaginationData(data);
182
204
  if (!paginationData) return;
183
205
 
184
206
  if (
185
207
  paginationData.previous_page !== paginationData.current_page &&
186
208
  paginationData.previous_page < paginationData.current_page
187
209
  ) {
188
- setRequestPath(constructPaginationLink(requestPath, paginationData.previous_page));
210
+ const newPath = constructPaginationLink(requestPath, paginationData.previous_page);
211
+ setRequestPath(newPath);
212
+ setPage(paginationData.previous_page);
189
213
  }
190
- };
214
+ }, [query.data, getPaginationData, constructPaginationLink, requestPath]);
191
215
 
192
216
  /**
193
- * Construct a pagination URL using the configured builder
217
+ * Navigate to a specific page
194
218
  */
195
- const constructPaginationLink = (link: string, pageNumber: number) => {
196
- // Use the configured pagination URL builder or fall back to default
197
- const buildPaginationUrl =
198
- pagination.buildPaginationUrl ||
199
- ((url, page) => {
200
- const [pathname, queryString] = url.split('?');
201
- const queryParams = new URLSearchParams(queryString || '');
202
- const pageParamName = pagination.pageParamName || 'page';
203
-
204
- const oldPage = Number(queryParams.get(pageParamName));
205
- queryParams.set(pageParamName, String(page));
206
-
207
- const newUrl = pathname + '?' + queryParams.toString();
208
-
209
- // only update page when pagination number changed
210
- if (oldPage !== pageNumber) {
211
- setPage(pageNumber);
212
- }
213
-
214
- return newUrl;
215
- });
216
-
217
- return buildPaginationUrl(link, pageNumber);
218
- };
219
+ const gotoPage = useCallback(
220
+ (pageNumber: number) => {
221
+ const newPath = constructPaginationLink(requestPath, pageNumber);
222
+ setRequestPath(newPath);
223
+ setPage(pageNumber);
224
+ },
225
+ [constructPaginationLink, requestPath]
226
+ );
219
227
 
220
228
  /**
221
- * Navigate to a specific page
229
+ * Imperative GET request - fetches data from a dynamic URL
230
+ * Uses queryClient.fetchQuery for proper caching and deduplication
231
+ *
232
+ * @param url - The URL to fetch from
233
+ * @param fetchOptions - Optional query options (staleTime, gcTime, etc.)
234
+ * @returns Promise resolving to the response data
222
235
  */
223
- const gotoPage = (pageNumber: number) => {
224
- setRequestPath(constructPaginationLink(requestPath, pageNumber));
225
- };
226
-
227
- const updatedPathAsync = async (link: string) => {
228
- startTransition(() => {
229
- setRequestPath(link);
230
- });
231
- };
232
-
233
- const setOptionsAsync = async (fetchOptions: any) => {
234
- startTransition(() => {
235
- setOptions(fetchOptions);
236
- });
237
- };
236
+ const get = useCallback(
237
+ async (
238
+ url: string,
239
+ fetchOptions?: {
240
+ staleTime?: number;
241
+ gcTime?: number;
242
+ }
243
+ ): Promise<IRequestSuccess<TResponse>> => {
244
+ if (isFutureQueriesPaused) {
245
+ throw new Error('Queries are currently paused');
246
+ }
247
+
248
+ // Use fetchQuery for imperative fetching - this properly handles caching
249
+ const result = await queryClient.fetchQuery({
250
+ queryKey: [url, {}] as const,
251
+ queryFn: () => executeRequest(url),
252
+ staleTime: fetchOptions?.staleTime,
253
+ gcTime: fetchOptions?.gcTime,
254
+ });
238
255
 
239
- const get = async (
240
- link: string,
241
- fetchOptions?: UseQueryOptions<
242
- IRequestSuccess<TResponse | undefined>,
243
- IRequestError,
244
- IRequestSuccess<TResponse | undefined>,
245
- Array<any>
246
- >
247
- ): Promise<IRequestSuccess<TResponse> | undefined> => {
248
- if (!isFutureQueriesPaused) {
249
- await setOptionsAsync(fetchOptions);
250
- await updatedPathAsync(link);
251
-
252
- return query.data;
253
- } else {
254
- setRequestPayload({ link, fetchOptions });
255
- return undefined;
256
- }
257
- };
256
+ return result;
257
+ },
258
+ [queryClient, executeRequest, isFutureQueriesPaused]
259
+ );
258
260
 
259
- useEffect(() => {
260
- if (!isFutureQueriesPaused && requestPayload) {
261
- get(requestPayload.link, requestPayload.fetchOptions);
262
- setRequestPayload(undefined);
263
- }
264
- // eslint-disable-next-line react-hooks/exhaustive-deps
265
- }, [isFutureQueriesPaused]);
261
+ /**
262
+ * Refetch the current query with the existing path
263
+ */
264
+ const refetch = useCallback(() => {
265
+ return query.refetch();
266
+ }, [query]);
266
267
 
267
268
  return {
268
269
  ...query,
269
- isLoading: (query.isLoading as boolean) || isFutureQueriesPaused,
270
+ isLoading: query.isLoading || isFutureQueriesPaused,
270
271
  setRequestPath,
271
272
  nextPage,
272
273
  prevPage,
273
274
  get,
274
275
  gotoPage,
275
276
  page,
276
- queryKey: [requestPath, {}],
277
- // Add pagination data accessor - restructured to avoid linter error
278
- getPaginationData: function () {
279
- return (query as any).data ? getPaginationData(query.data) : undefined;
277
+ refetch,
278
+ queryKey: [requestPath, {}] as const,
279
+ getPaginationData: () => {
280
+ const data = query.data as IRequestSuccess<TResponse> | undefined;
281
+ return data ? getPaginationData(data) : undefined;
280
282
  },
281
283
  };
282
284
  };
@@ -4,7 +4,6 @@ import { useStore } from '@tanstack/react-store';
4
4
  import { useEffect, useMemo, useState } from 'react';
5
5
  import { useEnvironmentVariables } from '../config';
6
6
  import { bootStore } from '../config/bootStore';
7
- import { scrollToTop } from '../helpers';
8
7
  import { useUploadProgress } from '../hooks';
9
8
  import { HttpMethod, makeRequest } from '../request';
10
9
  import type { IRequestError, IRequestSuccess } from '../request/request.interface';
@@ -14,7 +13,7 @@ import type { DefaultRequestOptions } from './queries.interface';
14
13
  export const usePatchRequest = <TResponse>({ path, baseUrl, headers }: { path: string } & DefaultRequestOptions) => {
15
14
  const { API_URL, TIMEOUT } = useEnvironmentVariables();
16
15
  const { uploadProgressPercent, onUploadProgress } = useUploadProgress();
17
- const { context, headerProvider } = useStore(bootStore);
16
+ const { headerProvider } = useStore(bootStore);
18
17
 
19
18
  const storeHeaders = useHeaderStore((state) => state.headers);
20
19
 
@@ -61,16 +60,8 @@ export const usePatchRequest = <TResponse>({ path, baseUrl, headers }: { path: s
61
60
  const patchResponse = await makeRequest<TResponse>(requestOptions);
62
61
  // }
63
62
  if (patchResponse.status) {
64
- // scroll to top after success
65
- if (context !== 'app') {
66
- scrollToTop();
67
- }
68
63
  res(patchResponse as IRequestSuccess<TResponse>);
69
64
  } else {
70
- // scroll to top after error
71
- if (context !== 'app') {
72
- scrollToTop();
73
- }
74
65
  rej(patchResponse);
75
66
  }
76
67
  };
@@ -5,7 +5,6 @@ import { useEnvironmentVariables, useReactNativeEnv } from '../config';
5
5
  import { useStore } from '@tanstack/react-store';
6
6
  import { useEffect, useMemo, useState } from 'react';
7
7
  import { bootStore } from '../config/bootStore';
8
- import { scrollToTop } from '../helpers';
9
8
  import { useUploadProgress } from '../hooks';
10
9
  import type { IMakeRequest, IRequestError, IRequestSuccess } from '../request';
11
10
  import { HttpMethod, makeRequest } from '../request';
@@ -25,7 +24,7 @@ export const usePostRequest = <TResponse>({
25
24
  } & DefaultRequestOptions) => {
26
25
  const { API_URL, TIMEOUT } = useEnvironmentVariables();
27
26
 
28
- const { context, headerProvider } = useStore(bootStore);
27
+ const { headerProvider } = useStore(bootStore);
29
28
 
30
29
  const storeHeaders = useHeaderStore((state) => state.headers);
31
30
 
@@ -88,17 +87,8 @@ export const usePostRequest = <TResponse>({
88
87
  // }
89
88
 
90
89
  if (postResponse.status) {
91
- // scroll to top after success
92
-
93
- if (context !== 'app') {
94
- scrollToTop();
95
- }
96
90
  res(postResponse as IRequestSuccess<TResponse>);
97
91
  } else {
98
- // scroll to top after error
99
- if (context !== 'app') {
100
- scrollToTop();
101
- }
102
92
  rej(postResponse);
103
93
  }
104
94
  };
@@ -4,7 +4,6 @@ import { useStore } from '@tanstack/react-store';
4
4
  import { useEffect, useMemo, useState } from 'react';
5
5
  import { useEnvironmentVariables } from '../config';
6
6
  import { bootStore } from '../config/bootStore';
7
- import { scrollToTop } from '../helpers';
8
7
  import { useUploadProgress } from '../hooks';
9
8
  import { HttpMethod, makeRequest } from '../request';
10
9
  import type { IRequestError, IRequestSuccess } from '../request/request.interface';
@@ -14,7 +13,7 @@ import type { DefaultRequestOptions } from './queries.interface';
14
13
  export const usePutRequest = <TResponse>({ path, baseUrl, headers }: { path: string } & DefaultRequestOptions) => {
15
14
  const { API_URL, TIMEOUT } = useEnvironmentVariables();
16
15
  const { uploadProgressPercent, onUploadProgress } = useUploadProgress();
17
- const { context, headerProvider } = useStore(bootStore);
16
+ const { headerProvider } = useStore(bootStore);
18
17
 
19
18
  const storeHeaders = useHeaderStore((state) => state.headers);
20
19
 
@@ -57,16 +56,8 @@ export const usePutRequest = <TResponse>({ path, baseUrl, headers }: { path: str
57
56
  const putResponse = await makeRequest<TResponse>(requestOptions);
58
57
  // }
59
58
  if (putResponse.status) {
60
- // scroll to top after success
61
- if (context !== 'app') {
62
- scrollToTop();
63
- }
64
59
  res(putResponse as IRequestSuccess<TResponse>);
65
60
  } else {
66
- // scroll to top after error
67
- if (context !== 'app') {
68
- scrollToTop();
69
- }
70
61
  rej(putResponse);
71
62
  }
72
63
  };
@@ -1 +0,0 @@
1
- export declare const scrollToTop: () => void;
@@ -1,9 +0,0 @@
1
- const scrollToTop = () => {
2
- window.scrollTo({
3
- top: 0,
4
- behavior: 'smooth',
5
- });
6
- };
7
-
8
- export { scrollToTop };
9
- //# sourceMappingURL=scrollToTop.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scrollToTop.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -1,6 +0,0 @@
1
- export const scrollToTop = () => {
2
- window.scrollTo({
3
- top: 0,
4
- behavior: 'smooth',
5
- });
6
- };