@pixotope/query 0.4.0 → 0.6.0

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.
@@ -13,10 +13,13 @@ export interface MutationOptions<
13
13
  mutationFn: MutationFunction<TData, TParams>;
14
14
  params?: TParams;
15
15
  onSuccess?: (data: TData, variables: TParams) => Promise<unknown> | unknown;
16
- onError?: (error: TError, variables: TParams) => Promise<unknown> | unknown;
16
+ onError?: (
17
+ error: TMutationError<TError>,
18
+ variables: TParams
19
+ ) => Promise<unknown> | unknown;
17
20
  onSettled?: (
18
21
  data: TData | undefined,
19
- error: TError | null,
22
+ error: TMutationError<TError> | null,
20
23
  variables: TParams
21
24
  ) => Promise<unknown> | unknown;
22
25
  compareOrThrow?: (result: TData | TError) => boolean;
@@ -24,16 +27,18 @@ export interface MutationOptions<
24
27
  prepareParams?: (variables: TParams, defaults?: TParams) => TParams;
25
28
  }
26
29
 
30
+ type TMutationError<TError> = TError | Error | null;
31
+
27
32
  export interface MutateOptions<
28
33
  TData = unknown,
29
34
  TError = unknown,
30
35
  TParams = void,
31
36
  > {
32
37
  onSuccess?: (data: TData, variables: TParams) => void;
33
- onError?: (error: TError, variables: TParams) => void;
38
+ onError?: (error: TMutationError<TError>, variables: TParams) => void;
34
39
  onSettled?: (
35
40
  data: TData | undefined,
36
- error: TError | null,
41
+ error: TMutationError<TError> | null,
37
42
  variables: TParams
38
43
  ) => void;
39
44
  }
@@ -102,7 +107,11 @@ export class ComparisonFailError<TPayload = unknown> extends Error {
102
107
  * });
103
108
  * ```
104
109
  */
105
- export const useMutation = <TData = unknown, TError = unknown, TParams = void>(
110
+ export const useMutation = <
111
+ TData = unknown,
112
+ TError = TMutationError<unknown>,
113
+ TParams = void,
114
+ >(
106
115
  options: MutationOptions<TData, TError, TParams>
107
116
  ) => {
108
117
  const {
@@ -116,7 +125,7 @@ export const useMutation = <TData = unknown, TError = unknown, TParams = void>(
116
125
  timeout,
117
126
  } = options;
118
127
  const [status, setStatus] = useState<MutationStatus>("idle");
119
- const [error, setError] = useState<TError | null>(null);
128
+ const [error, setError] = useState<TMutationError<TError>>(null);
120
129
  const [result, setResult] = useState<TData | undefined>(undefined);
121
130
  const mutationFnRef = useRef(mutationFn);
122
131
  const internalOptionsRef = useRef({
@@ -182,13 +191,8 @@ export const useMutation = <TData = unknown, TError = unknown, TParams = void>(
182
191
  setError(error);
183
192
  setStatus("error");
184
193
 
185
- if (internalCallbackRef.current.onError) {
186
- internalCallbackRef.current.onError(error, params);
187
- }
188
-
189
- if (internalCallbackRef.current.onSettled) {
190
- internalCallbackRef.current.onSettled(undefined, error, params);
191
- }
194
+ internalCallbackRef.current.onError?.(error, params);
195
+ internalCallbackRef.current.onSettled?.(undefined, error, params);
192
196
 
193
197
  throw error;
194
198
  }
@@ -222,7 +226,11 @@ export const useMutation = <TData = unknown, TError = unknown, TParams = void>(
222
226
  status,
223
227
  result,
224
228
  mutate,
225
- reset: () => setStatus("idle"),
229
+ reset: () => {
230
+ setStatus("idle");
231
+ setError(null);
232
+ setResult(undefined);
233
+ },
226
234
  error,
227
235
  };
228
236
  };
@@ -242,13 +242,12 @@ describe("useQuery", () => {
242
242
  renderHook(
243
243
  (props) =>
244
244
  useQuery({
245
- queryFn: props.resolver,
245
+ queryFn: resolver,
246
246
  key: props.key,
247
247
  }),
248
248
  {
249
249
  initialProps: {
250
250
  key: ["dummyQK1"],
251
- resolver,
252
251
  },
253
252
  }
254
253
  )
@@ -257,9 +256,9 @@ describe("useQuery", () => {
257
256
  await waitFor(() => until(result.current.status === "success"), {
258
257
  timeout: 1000,
259
258
  });
260
- rerender({ key: ["dummyQK2"], resolver });
261
- rerender({ key: ["dummyQK2"], resolver });
262
- expect(resolver).toBeCalledTimes(2);
259
+ rerender({ key: ["dummyQK2"] });
260
+ rerender({ key: ["dummyQK3"] });
261
+ expect(resolver).toBeCalledTimes(3);
263
262
  });
264
263
  test("stateless mode", async () => {
265
264
  const resolver = vi.fn(() => defaultQueryMock.getMockedQueryFn());
package/src/useQuery.ts CHANGED
@@ -9,10 +9,10 @@ export interface QueryOptions<
9
9
  > {
10
10
  queryFn: (params: TParams | undefined | void) => Promise<TData>;
11
11
  onSuccess?: (data: TData) => Promise<void> | void;
12
- onError?: (error: TError) => Promise<void> | void;
12
+ onError?: (error: TQueryError<TError>) => Promise<void> | void;
13
13
  onSettled?: (
14
14
  data: TData | undefined,
15
- error: TError | undefined
15
+ error: TQueryError<TError> | undefined
16
16
  ) => Promise<void> | void;
17
17
  timeout?: number;
18
18
  enabled?: boolean;
@@ -32,6 +32,8 @@ type RacePromises<T> = Promise<T | RaceErrorReason>;
32
32
  type WithData<TData> = { data: TData | undefined };
33
33
  type TQueryKey = readonly any[];
34
34
 
35
+ type TQueryError<TError> = TError | Error | null;
36
+
35
37
  export type UseQueryResult<
36
38
  TData = unknown,
37
39
  TError = unknown,
@@ -39,7 +41,7 @@ export type UseQueryResult<
39
41
  TSelectData = TData,
40
42
  Stateless extends true | undefined = undefined,
41
43
  > = {
42
- error: TError | null;
44
+ error: TQueryError<TError>;
43
45
  status: QueryStatus;
44
46
  refetch: (params: TParams | void | undefined) => Promise<void>;
45
47
  reset: () => void;
@@ -51,11 +53,11 @@ function normalizeQueryKey(queryKey: TQueryKey): string {
51
53
  }
52
54
 
53
55
  type TInternalCallbackRef<TData, TError, TSelectData> = {
54
- onError?: (error: TError) => Promise<void> | void;
56
+ onError?: (error: TQueryError<TError>) => Promise<void> | void;
55
57
  onSuccess?: (data: TData) => Promise<void> | void;
56
58
  onSettled?: (
57
59
  data: TData | undefined,
58
- error: TError | undefined
60
+ error: TQueryError<TError> | undefined
59
61
  ) => Promise<void> | void;
60
62
  compareOrThrow?: (data: TData) => boolean;
61
63
  select?: (data: TData) => TSelectData;
@@ -70,9 +72,11 @@ type TInternalOptionsRef<TData> = {
70
72
  stateless?: boolean;
71
73
  };
72
74
 
75
+ const getRandomUUID = () => Math.random().toString(36).substring(2, 15);
76
+
73
77
  export const useQuery = <
74
78
  TData = unknown,
75
- TError = unknown,
79
+ TError = TQueryError<unknown>,
76
80
  TParams = void,
77
81
  TSelected = TData,
78
82
  Stateless extends true | undefined = undefined,
@@ -101,7 +105,6 @@ export const useQuery = <
101
105
  key: queryKeyPassed,
102
106
  } = options;
103
107
  const queryKey = normalizeQueryKey(queryKeyPassed ?? []);
104
- const promiseResolutionInProgress = useRef(false);
105
108
  const lastFetchedAt = useRef<number | null>(null);
106
109
  const lastQueryKey = useRef<string | undefined>(undefined);
107
110
  const queryFnRef = useRef(queryFn);
@@ -132,11 +135,12 @@ export const useQuery = <
132
135
  stateless,
133
136
  timeout,
134
137
  };
138
+ const requestIdRef = useRef(getRandomUUID());
135
139
 
136
140
  const [status, setStatus] = useState<QueryStatus>(() => {
137
141
  return "idle";
138
142
  });
139
- const [error, setError] = useState<TError | null>(null);
143
+ const [error, setError] = useState<TQueryError<TError>>(null);
140
144
  const [data, setData] = useState<TData | undefined>(() => {
141
145
  if (initialData !== undefined) {
142
146
  if (typeof initialData === "function") {
@@ -174,10 +178,9 @@ export const useQuery = <
174
178
 
175
179
  const refetch = useCallback(
176
180
  async (params: TParams | void | undefined = undefined) => {
177
- if (
178
- promiseResolutionInProgress.current ||
179
- !internalOptionsRef.current.enabled
180
- ) {
181
+ const requestId = getRandomUUID();
182
+ requestIdRef.current = requestId;
183
+ if (!internalOptionsRef.current.enabled) {
181
184
  return;
182
185
  }
183
186
 
@@ -199,9 +202,10 @@ export const useQuery = <
199
202
  : []),
200
203
  ];
201
204
 
202
- promiseResolutionInProgress.current = true;
203
205
  const res = await Promise.race(promises);
204
- promiseResolutionInProgress.current = false;
206
+ if (requestIdRef.current !== requestId) {
207
+ return;
208
+ }
205
209
 
206
210
  if (res === "timeout") {
207
211
  throw new Error("timeout");
@@ -213,7 +217,7 @@ export const useQuery = <
213
217
  internalCallbackRef.current.compareOrThrow &&
214
218
  internalCallbackRef.current.compareOrThrow(res) === true
215
219
  ) {
216
- throw new Error("compareOrThrow");
220
+ throw new Error("failedComparison");
217
221
  }
218
222
 
219
223
  setStatus("success");
@@ -238,42 +242,23 @@ export const useQuery = <
238
242
  }, [refetch]);
239
243
 
240
244
  useEffect(() => {
241
- if (enabled) {
242
- refetch();
243
- }
244
- }, [enabled, refetch, queryKey]);
245
-
246
- useEffect(() => {
247
- if (lastQueryKey.current === undefined) {
248
- lastQueryKey.current = queryKey;
249
- refetch();
250
- return;
251
- }
252
-
253
- if (queryKey !== lastQueryKey.current) {
254
- lastQueryKey.current = queryKey;
255
- refetch();
256
- }
257
- }, [queryKey, refetch, enabled]);
245
+ const cleanups: (() => void)[] = [];
258
246
 
259
- useEffect(() => {
247
+ // If the query should be refetched on window focus, add a listener
260
248
  if (refetchOnWindowFocus) {
261
249
  const queryHandleFocus = () => {
262
- if (shouldRefetch()) {
263
- refetch();
264
- }
250
+ refetch();
265
251
  };
266
252
 
267
253
  window.addEventListener("focus", queryHandleFocus);
268
254
 
269
- return () => {
255
+ cleanups.push(() => {
270
256
  window.removeEventListener("focus", queryHandleFocus);
271
- };
257
+ });
272
258
  }
273
- }, [refetchOnWindowFocus, refetch, shouldRefetch]);
274
259
 
275
- useEffect(() => {
276
- if (refetchInterval !== 0) {
260
+ // If the query should be refetched on an interval, add an interval
261
+ if (refetchInterval > 0) {
277
262
  const interval = setInterval(
278
263
  () => {
279
264
  if (shouldRefetch()) {
@@ -283,11 +268,31 @@ export const useQuery = <
283
268
  Math.max(refetchInterval, 1000)
284
269
  );
285
270
 
286
- return () => {
271
+ cleanups.push(() => {
287
272
  clearInterval(interval);
288
- };
273
+ });
289
274
  }
290
- }, [refetch, refetchInterval, shouldRefetch]);
275
+
276
+ // If the query key has changed, refetch
277
+ if (
278
+ lastQueryKey.current === undefined ||
279
+ queryKey !== lastQueryKey.current
280
+ ) {
281
+ lastQueryKey.current = queryKey;
282
+ refetch();
283
+ }
284
+
285
+ return () => {
286
+ cleanups.forEach((cleanup) => cleanup());
287
+ };
288
+ }, [
289
+ queryKey,
290
+ enabled,
291
+ refetchOnWindowFocus,
292
+ refetchInterval,
293
+ shouldRefetch,
294
+ refetch,
295
+ ]);
291
296
 
292
297
  return {
293
298
  data: