@kirill.konshin/react 0.0.1 → 0.0.3

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/src/useFetch.ts CHANGED
@@ -1,29 +1,109 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useState, useTransition } from 'react';
3
+ import { useCallback, useEffect, useRef, useState, useTransition } from 'react';
4
4
 
5
- //TODO useFetch https://use-http.com
6
- //TODO SWR?
7
- //TODO Tanstack Query?
5
+ const unmountError = 'Component is unmounted after fetch completed';
6
+
7
+ /**
8
+ * TODO useFetch https://use-http.com
9
+ * TODO SWR?
10
+ * TODO Tanstack Query?
11
+ *
12
+ * Uses same return array patterns as useActionState https://react.dev/reference/react/useActionState + error,
13
+ * reason: simple var rename
14
+ *
15
+ * Function can be async, then it will be awaited and result set to state.
16
+ *
17
+ * Function can be sync, then it will be called args, and it should return another function,which will be called with
18
+ * old data and result set to state. This is useful for pagination and merging data.
19
+ *
20
+ * @param {(...args: any[]) => Promise<R> | R | ((oldData: R) => Promise<R> | R))} fn
21
+ * @param {any} defaultValue
22
+ * @param fetchOnMount
23
+ * @param throwAfterUnmount - throw if component is unmounted after fetch completed
24
+ * @returns {[R, (...args: any[]) => Promise<R>, boolean, Error | undefined]} //, ReturnType<typeof useState>, ReturnType<typeof useState>
25
+ */
8
26
  export function useFetch<R>(
9
- fn: (...args: any[]) => Promise<R>,
10
- defaultValue: R | null = null,
11
- ): [R | null, typeof fn, boolean, Error | undefined] {
27
+ fn: (...args: any[]) => Promise<R> | R | ((oldData: R) => Promise<R> | R),
28
+ defaultValue: R,
29
+ {
30
+ fetchOnMount = false,
31
+ throwAfterUnmount = false,
32
+ }: {
33
+ fetchOnMount?: boolean;
34
+ throwAfterUnmount?: boolean;
35
+ } = {},
36
+ ): [
37
+ R,
38
+ (...args: Parameters<typeof fn>) => Promise<R>,
39
+ boolean,
40
+ Error | undefined,
41
+ // ReturnType<typeof useState<R>>[1],
42
+ // ReturnType<typeof useState<Error>>[1],
43
+ ] {
12
44
  // An async function was passed to useActionState, but it was dispatched outside of an action context.
13
45
  // This is likely not what you intended. Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`
14
46
  const [isPending, startTransition] = useTransition();
15
- const [data, setData] = useState<R | null>(defaultValue);
47
+ const [data, setData] = useState<R>(defaultValue);
16
48
  const [error, setError] = useState<Error>();
49
+ const [loading, setLoading] = useState(fetchOnMount);
50
+ const isMounted = useRef(false);
51
+ const oldData = useRef(data);
52
+ const throwAfterUnmountRef = useRef(throwAfterUnmount);
17
53
 
18
54
  const actionFn = useCallback(
19
55
  (...args: Parameters<typeof fn>) => {
20
- const promise = fn(...args);
56
+ const res = fn(...args);
57
+
58
+ const promise: Promise<R> = typeof res === 'function' ? (res as any)(oldData.current) : (res as Promise<R>);
59
+
21
60
  // https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition
22
- startTransition(() => promise.then(setData).catch(setError)); //FIXME sub-chain...
61
+ startTransition(async () => {
62
+ try {
63
+ const newData = await promise;
64
+ if (!isMounted.current) {
65
+ if (throwAfterUnmountRef.current) throw new Error(unmountError);
66
+ return;
67
+ }
68
+ oldData.current = newData;
69
+ setData(newData);
70
+ setError(undefined);
71
+ } catch (e) {
72
+ if (!isMounted.current) {
73
+ if (throwAfterUnmountRef.current) {
74
+ if (e.message !== unmountError) throw new Error('Component is unmounted', { cause: e });
75
+ else throw e;
76
+ }
77
+ return;
78
+ }
79
+ setError(e);
80
+ } finally {
81
+ if (isMounted.current) {
82
+ setLoading(false);
83
+ }
84
+ }
85
+ });
86
+
23
87
  return promise;
24
88
  },
25
89
  [fn],
26
90
  );
27
91
 
28
- return [data, actionFn, isPending, error];
92
+ useEffect(() => {
93
+ if (!fetchOnMount) return;
94
+ actionFn().catch((e) => console.error('Fetch on mount failed', e)); // catch actually will never happen
95
+ }, [fetchOnMount, fn, actionFn]);
96
+
97
+ useEffect(() => {
98
+ throwAfterUnmountRef.current = throwAfterUnmount;
99
+ }, [throwAfterUnmount]);
100
+
101
+ useEffect(() => {
102
+ isMounted.current = true;
103
+ return () => {
104
+ isMounted.current = false;
105
+ };
106
+ });
107
+
108
+ return [data, actionFn, isPending || loading, error]; // , setData, setError
29
109
  }