@kirill.konshin/react 0.0.2 → 0.0.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.
@@ -12,14 +12,19 @@ transforming...
12
12
  rendering chunks...
13
13
 
14
14
  [vite:dts] Start generate declaration files...
15
- dist/useFetch.js 0.57 kB map: 1.69 kB
16
- dist/index.js 0.67 kB │ map: 0.10 kB
17
- dist/apiCall.js 0.69 kB │ map: 1.45 kB
18
- dist/useFetcher.js 1.18 kB │ map: 2.62 kB
19
- dist/keyboard.js 1.27 kB │ map: 2.81 kB
20
- dist/form/client.js 1.58 kB │ map: 3.88 kB
21
- dist/form/form.js 2.89 kB │ map: 7.94 kB
22
- [vite:dts] Declaration files built in 1853ms.
15
+ src/index.ts:4:15 - error TS2307: Cannot find module './useFetcher' or its corresponding type declarations.
23
16
 
24
- ✓ built in 5.02s
17
+ 4 export * from './useFetcher';
18
+    ~~~~~~~~~~~~~~
19
+
20
+ dist/index.js 0.68 kB │ map: 0.10 kB
21
+ dist/apiCall.js 0.69 kB │ map: 1.45 kB
22
+ dist/useFetchAction.js 0.79 kB │ map: 1.90 kB
23
+ dist/keyboard.js 1.27 kB │ map: 2.81 kB
24
+ dist/form/client.js 1.58 kB │ map: 3.88 kB
25
+ dist/useFetch.js 2.04 kB │ map: 5.62 kB
26
+ dist/form/form.js 2.89 kB │ map: 7.94 kB
27
+ [vite:dts] Declaration files built in 2074ms.
28
+
29
+ ✓ built in 6.65s
25
30
  Updated package.json with exports
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @kirill.konshin/react
2
2
 
3
+ ## 0.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - useFetchAction
8
+
9
+ ## 0.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Minor fixes
14
+
3
15
  ## 0.0.2
4
16
 
5
17
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # `useFetch`
2
+
3
+ ```tsx
4
+ import { useEffect } from 'react';
5
+ import { useFetch } from '@kirill.konshin/react';
6
+
7
+ const [data, actionFn, isPending, error] = useFetch(async (args) => {
8
+ return callToBackend(args);
9
+ }, defaultValue);
10
+
11
+ useEffect(() => {
12
+ actionFn({ id: 1 });
13
+ }, []);
14
+ ```
15
+
16
+ ```tsx
17
+ import { useEffect } from 'react';
18
+ import { useFetch } from '@kirill.konshin/react';
19
+
20
+ const dataRef = useRef(data);
21
+
22
+ const [data, actionFn, isPending, error] = useFetch(async (args) => {
23
+ const data = await callToBackend(args);
24
+ return [...dataRef.current, data];
25
+ }, defaultValue);
26
+
27
+ useEffect(() => {
28
+ dataRef.current = value;
29
+ }, [data]);
30
+
31
+ useEffect(async () => {
32
+ await actionFn({ id: 1 });
33
+ await actionFn({ id: 2 });
34
+ await actionFn({ id: 3 });
35
+ }, []);
36
+ ```
37
+
38
+ ```tsx
39
+ import { useEffect } from 'react';
40
+ import { useFetch } from '@kirill.konshin/react';
41
+
42
+ const [data, actionFn, isPending, error] = useFetch(
43
+ // first function has to be synchronous, second can be async
44
+ (args) => async (oldData) => {
45
+ const data = await callToBackend(args);
46
+ return [...oldData, data];
47
+ },
48
+ defaultValue,
49
+ );
50
+
51
+ useEffect(async () => {
52
+ await actionFn({ id: 1 });
53
+ await actionFn({ id: 2 });
54
+ await actionFn({ id: 3 });
55
+ }, []);
56
+ ```
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { apiCall, jsonContentType } from "./apiCall.js";
2
2
  import { HotkeysContext, HotkeysProvider, useHotkeys } from "./keyboard.js";
3
3
  import { useFetch } from "./useFetch.js";
4
- import { useFetcher } from "./useFetcher.js";
4
+ import { useFetchAction } from "./useFetchAction.js";
5
5
  import { createClient } from "./form/client.js";
6
6
  import { Field, Form, FormContext, Hint, create, isRequired, maxLength, minLength, stringRequired } from "./form/form.js";
7
7
  export {
@@ -20,7 +20,7 @@ export {
20
20
  minLength,
21
21
  stringRequired,
22
22
  useFetch,
23
- useFetcher,
23
+ useFetchAction,
24
24
  useHotkeys
25
25
  };
26
26
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,29 @@
1
- export declare function useFetch<R>(fn: (...args: any[]) => Promise<R>, defaultValue?: R | null): [R | null, typeof fn, boolean, Error | undefined];
1
+ /**
2
+ * TODO useFetch https://use-http.com
3
+ * TODO SWR?
4
+ * TODO Tanstack Query?
5
+ *
6
+ * Uses same return array patterns as useActionState https://react.dev/reference/react/useActionState + error,
7
+ * reason: simple var rename
8
+ *
9
+ * Function can be async, then it will be awaited and result set to state.
10
+ *
11
+ * Function can be sync, then it will be called args, and it should return another function,which will be called with
12
+ * old data and result set to state. This is useful for pagination and merging data.
13
+ *
14
+ * @param {(...args: any[]) => Promise<R> | R | ((oldData: R) => Promise<R> | R))} fn
15
+ * @param {any} defaultValue
16
+ * @param fetchOnMount
17
+ * @param throwAfterUnmount - throw if component is unmounted after fetch completed
18
+ * @returns {[R, (...args: any[]) => Promise<R>, boolean, Error | undefined]} //, ReturnType<typeof useState>, ReturnType<typeof useState>
19
+ */
20
+ export declare function useFetch<R>(fn: (...args: any[]) => Promise<R> | R | ((oldData: R) => Promise<R> | R), defaultValue: R, { fetchOnMount, throwAfterUnmount, }?: {
21
+ fetchOnMount?: boolean;
22
+ throwAfterUnmount?: boolean;
23
+ }): [
24
+ R,
25
+ (...args: Parameters<typeof fn>) => Promise<R>,
26
+ boolean,
27
+ Error | undefined
28
+ ];
2
29
  //# sourceMappingURL=useFetch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFetch.d.ts","sourceRoot":"","sources":["../src/useFetch.ts"],"names":[],"mappings":"AAOA,wBAAgB,QAAQ,CAAC,CAAC,EACtB,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,EAClC,YAAY,GAAE,CAAC,GAAG,IAAW,GAC9B,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,GAAG,SAAS,CAAC,CAkBnD"}
1
+ {"version":3,"file":"useFetch.d.ts","sourceRoot":"","sources":["../src/useFetch.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACtB,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EACzE,YAAY,EAAE,CAAC,EACf,EACI,YAAoB,EACpB,iBAAyB,GAC5B,GAAE;IACC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC1B,GACP;IACC,CAAC;IACD,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC;IAC9C,OAAO;IACP,KAAK,GAAG,SAAS;CAGpB,CAkEA"}
package/dist/useFetch.js CHANGED
@@ -1,18 +1,64 @@
1
1
  "use client";
2
- import { useTransition, useState, useCallback } from "react";
3
- function useFetch(fn, defaultValue = null) {
2
+ import { useTransition, useState, useRef, useCallback, useEffect } from "react";
3
+ const unmountError = "Component is unmounted after fetch completed";
4
+ function useFetch(fn, defaultValue, {
5
+ fetchOnMount = false,
6
+ throwAfterUnmount = false
7
+ } = {}) {
4
8
  const [isPending, startTransition] = useTransition();
5
9
  const [data, setData] = useState(defaultValue);
6
10
  const [error, setError] = useState();
11
+ const [loading, setLoading] = useState(fetchOnMount);
12
+ const isMounted = useRef(false);
13
+ const oldData = useRef(data);
14
+ const throwAfterUnmountRef = useRef(throwAfterUnmount);
7
15
  const actionFn = useCallback(
8
16
  (...args) => {
9
- const promise = fn(...args);
10
- startTransition(() => promise.then(setData).catch(setError));
17
+ const res = fn(...args);
18
+ const promise = typeof res === "function" ? res(oldData.current) : res;
19
+ startTransition(async () => {
20
+ try {
21
+ const newData = await promise;
22
+ if (!isMounted.current) {
23
+ if (throwAfterUnmountRef.current) throw new Error(unmountError);
24
+ return;
25
+ }
26
+ oldData.current = newData;
27
+ setData(newData);
28
+ setError(void 0);
29
+ } catch (e) {
30
+ if (!isMounted.current) {
31
+ if (throwAfterUnmountRef.current) {
32
+ if (e.message !== unmountError) throw new Error("Component is unmounted", { cause: e });
33
+ else throw e;
34
+ }
35
+ return;
36
+ }
37
+ setError(e);
38
+ } finally {
39
+ if (isMounted.current) {
40
+ setLoading(false);
41
+ }
42
+ }
43
+ });
11
44
  return promise;
12
45
  },
13
46
  [fn]
14
47
  );
15
- return [data, actionFn, isPending, error];
48
+ useEffect(() => {
49
+ if (!fetchOnMount) return;
50
+ actionFn().catch((e) => console.error("Fetch on mount failed", e));
51
+ }, [fetchOnMount, fn, actionFn]);
52
+ useEffect(() => {
53
+ throwAfterUnmountRef.current = throwAfterUnmount;
54
+ }, [throwAfterUnmount]);
55
+ useEffect(() => {
56
+ isMounted.current = true;
57
+ return () => {
58
+ isMounted.current = false;
59
+ };
60
+ });
61
+ return [data, actionFn, isPending || loading, error];
16
62
  }
17
63
  export {
18
64
  useFetch
@@ -1 +1 @@
1
- {"version":3,"file":"useFetch.js","sources":["../src/useFetch.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useState, useTransition } from 'react';\n\n//TODO useFetch https://use-http.com\n//TODO SWR?\n//TODO Tanstack Query?\nexport function useFetch<R>(\n fn: (...args: any[]) => Promise<R>,\n defaultValue: R | null = null,\n): [R | null, typeof fn, boolean, Error | undefined] {\n // An async function was passed to useActionState, but it was dispatched outside of an action context.\n // This is likely not what you intended. Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`\n const [isPending, startTransition] = useTransition();\n const [data, setData] = useState<R | null>(defaultValue);\n const [error, setError] = useState<Error>();\n\n const actionFn = useCallback(\n (...args: Parameters<typeof fn>) => {\n const promise = fn(...args);\n // https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition\n startTransition(() => promise.then(setData).catch(setError)); //FIXME sub-chain...\n return promise;\n },\n [fn],\n );\n\n return [data, actionFn, isPending, error];\n}\n"],"names":[],"mappings":";;AAOO,SAAS,SACZ,IACA,eAAyB,MACwB;AAGjD,QAAM,CAAC,WAAW,eAAe,IAAI,cAAA;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,YAAY;AACvD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAA;AAE1B,QAAM,WAAW;AAAA,IACb,IAAI,SAAgC;AAChC,YAAM,UAAU,GAAG,GAAG,IAAI;AAE1B,sBAAgB,MAAM,QAAQ,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3D,aAAO;AAAA,IACX;AAAA,IACA,CAAC,EAAE;AAAA,EAAA;AAGP,SAAO,CAAC,MAAM,UAAU,WAAW,KAAK;AAC5C;"}
1
+ {"version":3,"file":"useFetch.js","sources":["../src/useFetch.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState, useTransition } from 'react';\n\nconst unmountError = 'Component is unmounted after fetch completed';\n\n/**\n * TODO useFetch https://use-http.com\n * TODO SWR?\n * TODO Tanstack Query?\n *\n * Uses same return array patterns as useActionState https://react.dev/reference/react/useActionState + error,\n * reason: simple var rename\n *\n * Function can be async, then it will be awaited and result set to state.\n *\n * Function can be sync, then it will be called args, and it should return another function,which will be called with\n * old data and result set to state. This is useful for pagination and merging data.\n *\n * @param {(...args: any[]) => Promise<R> | R | ((oldData: R) => Promise<R> | R))} fn\n * @param {any} defaultValue\n * @param fetchOnMount\n * @param throwAfterUnmount - throw if component is unmounted after fetch completed\n * @returns {[R, (...args: any[]) => Promise<R>, boolean, Error | undefined]} //, ReturnType<typeof useState>, ReturnType<typeof useState>\n */\nexport function useFetch<R>(\n fn: (...args: any[]) => Promise<R> | R | ((oldData: R) => Promise<R> | R),\n defaultValue: R,\n {\n fetchOnMount = false,\n throwAfterUnmount = false,\n }: {\n fetchOnMount?: boolean;\n throwAfterUnmount?: boolean;\n } = {},\n): [\n R,\n (...args: Parameters<typeof fn>) => Promise<R>,\n boolean,\n Error | undefined,\n // ReturnType<typeof useState<R>>[1],\n // ReturnType<typeof useState<Error>>[1],\n] {\n // An async function was passed to useActionState, but it was dispatched outside of an action context.\n // This is likely not what you intended. Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`\n const [isPending, startTransition] = useTransition();\n const [data, setData] = useState<R>(defaultValue);\n const [error, setError] = useState<Error>();\n const [loading, setLoading] = useState(fetchOnMount);\n const isMounted = useRef(false);\n const oldData = useRef(data);\n const throwAfterUnmountRef = useRef(throwAfterUnmount);\n\n const actionFn = useCallback(\n (...args: Parameters<typeof fn>) => {\n const res = fn(...args);\n\n const promise: Promise<R> = typeof res === 'function' ? (res as any)(oldData.current) : (res as Promise<R>);\n\n // https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition\n startTransition(async () => {\n try {\n const newData = await promise;\n if (!isMounted.current) {\n if (throwAfterUnmountRef.current) throw new Error(unmountError);\n return;\n }\n oldData.current = newData;\n setData(newData);\n setError(undefined);\n } catch (e) {\n if (!isMounted.current) {\n if (throwAfterUnmountRef.current) {\n if (e.message !== unmountError) throw new Error('Component is unmounted', { cause: e });\n else throw e;\n }\n return;\n }\n setError(e);\n } finally {\n if (isMounted.current) {\n setLoading(false);\n }\n }\n });\n\n return promise;\n },\n [fn],\n );\n\n useEffect(() => {\n if (!fetchOnMount) return;\n actionFn().catch((e) => console.error('Fetch on mount failed', e)); // catch actually will never happen\n }, [fetchOnMount, fn, actionFn]);\n\n useEffect(() => {\n throwAfterUnmountRef.current = throwAfterUnmount;\n }, [throwAfterUnmount]);\n\n useEffect(() => {\n isMounted.current = true;\n return () => {\n isMounted.current = false;\n };\n });\n\n return [data, actionFn, isPending || loading, error]; // , setData, setError\n}\n"],"names":[],"mappings":";;AAIA,MAAM,eAAe;AAqBd,SAAS,SACZ,IACA,cACA;AAAA,EACI,eAAe;AAAA,EACf,oBAAoB;AACxB,IAGI,IAQN;AAGE,QAAM,CAAC,WAAW,eAAe,IAAI,cAAA;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAY,YAAY;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAA;AAC1B,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,YAAY;AACnD,QAAM,YAAY,OAAO,KAAK;AAC9B,QAAM,UAAU,OAAO,IAAI;AAC3B,QAAM,uBAAuB,OAAO,iBAAiB;AAErD,QAAM,WAAW;AAAA,IACb,IAAI,SAAgC;AAChC,YAAM,MAAM,GAAG,GAAG,IAAI;AAEtB,YAAM,UAAsB,OAAO,QAAQ,aAAc,IAAY,QAAQ,OAAO,IAAK;AAGzF,sBAAgB,YAAY;AACxB,YAAI;AACA,gBAAM,UAAU,MAAM;AACtB,cAAI,CAAC,UAAU,SAAS;AACpB,gBAAI,qBAAqB,QAAS,OAAM,IAAI,MAAM,YAAY;AAC9D;AAAA,UACJ;AACA,kBAAQ,UAAU;AAClB,kBAAQ,OAAO;AACf,mBAAS,MAAS;AAAA,QACtB,SAAS,GAAG;AACR,cAAI,CAAC,UAAU,SAAS;AACpB,gBAAI,qBAAqB,SAAS;AAC9B,kBAAI,EAAE,YAAY,aAAc,OAAM,IAAI,MAAM,0BAA0B,EAAE,OAAO,GAAG;AAAA,kBACjF,OAAM;AAAA,YACf;AACA;AAAA,UACJ;AACA,mBAAS,CAAC;AAAA,QACd,UAAA;AACI,cAAI,UAAU,SAAS;AACnB,uBAAW,KAAK;AAAA,UACpB;AAAA,QACJ;AAAA,MACJ,CAAC;AAED,aAAO;AAAA,IACX;AAAA,IACA,CAAC,EAAE;AAAA,EAAA;AAGP,YAAU,MAAM;AACZ,QAAI,CAAC,aAAc;AACnB,aAAA,EAAW,MAAM,CAAC,MAAM,QAAQ,MAAM,yBAAyB,CAAC,CAAC;AAAA,EACrE,GAAG,CAAC,cAAc,IAAI,QAAQ,CAAC;AAE/B,YAAU,MAAM;AACZ,yBAAqB,UAAU;AAAA,EACnC,GAAG,CAAC,iBAAiB,CAAC;AAEtB,YAAU,MAAM;AACZ,cAAU,UAAU;AACpB,WAAO,MAAM;AACT,gBAAU,UAAU;AAAA,IACxB;AAAA,EACJ,CAAC;AAED,SAAO,CAAC,MAAM,UAAU,aAAa,SAAS,KAAK;AACvD;"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useFetch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFetch.test.d.ts","sourceRoot":"","sources":["../src/useFetch.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {(state: Awaited<R>, payload?: any) => Promise<R> | R} action
3
+ * @param {R} defaultValue
4
+ * @param {boolean} fetchOnMount
5
+ * @param {string} permalink
6
+ * @returns {[R, (payload?: P) => Promise<R> | R, boolean]}>
7
+ */
8
+ export declare function useFetchAction<S, P = undefined>(action: (state: Awaited<S>, payload?: P) => S | Promise<S>, defaultValue: Awaited<S>, { fetchOnMount, permalink, }?: {
9
+ fetchOnMount?: boolean;
10
+ permalink?: string;
11
+ }): [S, (payload: P) => void, boolean];
12
+ //# sourceMappingURL=useFetchAction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFetchAction.d.ts","sourceRoot":"","sources":["../src/useFetchAction.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC1D,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,EACxB,EACI,YAAoB,EACpB,SAAqB,GACxB,GAAE;IACC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACjB,GACP,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,EAAE,OAAO,CAAC,CAoBpC"}
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { useActionState, useState, useCallback, startTransition, useEffect } from "react";
3
+ function useFetchAction(action, defaultValue, {
4
+ fetchOnMount = false,
5
+ permalink = void 0
6
+ } = {}) {
7
+ const [data, dispatchAction, isPending] = useActionState(action, defaultValue, permalink);
8
+ const [loading, setLoading] = useState(fetchOnMount);
9
+ const dispatch = useCallback(
10
+ (payload) => {
11
+ startTransition(() => {
12
+ dispatchAction(payload);
13
+ setLoading(false);
14
+ });
15
+ },
16
+ [dispatchAction]
17
+ );
18
+ useEffect(() => {
19
+ if (!fetchOnMount || !loading) return;
20
+ dispatch(void 0);
21
+ }, [fetchOnMount, dispatch, loading]);
22
+ return [data, dispatch, isPending || loading];
23
+ }
24
+ export {
25
+ useFetchAction
26
+ };
27
+ //# sourceMappingURL=useFetchAction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFetchAction.js","sources":["../src/useFetchAction.ts"],"sourcesContent":["'use client';\n\nimport { useActionState, useCallback, useEffect, useState, startTransition } from 'react';\n\n/**\n * @param {(state: Awaited<R>, payload?: any) => Promise<R> | R} action\n * @param {R} defaultValue\n * @param {boolean} fetchOnMount\n * @param {string} permalink\n * @returns {[R, (payload?: P) => Promise<R> | R, boolean]}>\n */\nexport function useFetchAction<S, P = undefined>(\n action: (state: Awaited<S>, payload?: P) => S | Promise<S>,\n defaultValue: Awaited<S>,\n {\n fetchOnMount = false,\n permalink = undefined,\n }: {\n fetchOnMount?: boolean;\n permalink?: string;\n } = {},\n): [S, (payload: P) => void, boolean] {\n const [data, dispatchAction, isPending] = useActionState<S, P | undefined>(action, defaultValue, permalink);\n const [loading, setLoading] = useState(fetchOnMount);\n\n const dispatch = useCallback(\n (payload?: P) => {\n startTransition(() => {\n dispatchAction(payload);\n setLoading(false);\n });\n },\n [dispatchAction],\n );\n\n useEffect(() => {\n if (!fetchOnMount || !loading) return;\n dispatch(undefined);\n }, [fetchOnMount, dispatch, loading]);\n\n return [data, dispatch, isPending || loading];\n}\n"],"names":[],"mappings":";;AAWO,SAAS,eACZ,QACA,cACA;AAAA,EACI,eAAe;AAAA,EACf,YAAY;AAChB,IAGI,IAC8B;AAClC,QAAM,CAAC,MAAM,gBAAgB,SAAS,IAAI,eAAiC,QAAQ,cAAc,SAAS;AAC1G,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,YAAY;AAEnD,QAAM,WAAW;AAAA,IACb,CAAC,YAAgB;AACb,sBAAgB,MAAM;AAClB,uBAAe,OAAO;AACtB,mBAAW,KAAK;AAAA,MACpB,CAAC;AAAA,IACL;AAAA,IACA,CAAC,cAAc;AAAA,EAAA;AAGnB,YAAU,MAAM;AACZ,QAAI,CAAC,gBAAgB,CAAC,QAAS;AAC/B,aAAS,MAAS;AAAA,EACtB,GAAG,CAAC,cAAc,UAAU,OAAO,CAAC;AAEpC,SAAO,CAAC,MAAM,UAAU,aAAa,OAAO;AAChD;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kirill.konshin/react",
3
3
  "description": "Utilities",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "----- BUILD -----": "",
@@ -24,7 +24,13 @@
24
24
  },
25
25
  "devDependencies": {
26
26
  "@kirill.konshin/utils-private": "*",
27
- "react": "^19.1.1"
27
+ "@testing-library/dom": "^10.4.1",
28
+ "@testing-library/react": "^16.3.2",
29
+ "@testing-library/react-hooks": "^8.0.1",
30
+ "@types/react-dom": "^19",
31
+ "jsdom": "^29.0.1",
32
+ "react": "^19.1.1",
33
+ "react-dom": "19.1.1"
28
34
  },
29
35
  "peerDependencies": {
30
36
  "react": "^19"
@@ -35,7 +41,8 @@
35
41
  }
36
42
  },
37
43
  "publishConfig": {
38
- "access": "public"
44
+ "access": "public",
45
+ "provenance": true
39
46
  },
40
47
  "author": "Kirill Konshin <kirill@konshin.org> (https://konshin.org)",
41
48
  "license": "MIT",
@@ -55,5 +62,10 @@
55
62
  },
56
63
  "main": "./dist/index.js",
57
64
  "module": "./dist/index.js",
58
- "types": "./dist/index.d.ts"
65
+ "types": "./dist/index.d.ts",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/kirill-konshin/utils.git",
69
+ "directory": "packages/react"
70
+ }
59
71
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './apiCall';
2
2
  export * from './keyboard';
3
3
  export * from './useFetch';
4
- export * from './useFetcher';
4
+ export * from './useFetchAction';
5
5
  export * from './form';
@@ -0,0 +1,424 @@
1
+ import { expect, describe, test, vi } from 'vitest';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import { useFetch } from './useFetch';
4
+ import { act } from 'react';
5
+
6
+ describe('useFetch', () => {
7
+ test('initializes with default value', () => {
8
+ const mockFn = vi.fn(async () => 'result');
9
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
10
+
11
+ const [data, actionFn, isPending, error] = result.current;
12
+
13
+ expect(data).toBe('default');
14
+ expect(typeof actionFn).toBe('function');
15
+ expect(isPending).toBe(false);
16
+ expect(error).toBeUndefined();
17
+ });
18
+
19
+ test('executes function and updates data on success', async () => {
20
+ const mockFn = vi.fn(async () => 'success');
21
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
22
+
23
+ await act(async () => {
24
+ result.current[1]();
25
+ });
26
+
27
+ await waitFor(() => {
28
+ expect(result.current[2]).toBe(false); // isPending becomes false
29
+ });
30
+
31
+ expect(result.current[0]).toBe('success');
32
+ expect(result.current[3]).toBeUndefined();
33
+ expect(mockFn).toHaveBeenCalledTimes(1);
34
+ });
35
+
36
+ test('handles function arguments', async () => {
37
+ const mockFn = vi.fn(async (a: number, b: string) => `${a}-${b}`);
38
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
39
+
40
+ await act(async () => {
41
+ result.current[1](42, 'test');
42
+ });
43
+
44
+ await waitFor(() => {
45
+ expect(result.current[0]).toBe('42-test');
46
+ });
47
+
48
+ expect(mockFn).toHaveBeenCalledWith(42, 'test');
49
+ });
50
+
51
+ test('sets isPending to true during execution', async () => {
52
+ let resolvePromise: (value: string) => void;
53
+ const promise = new Promise<string>((resolve) => {
54
+ resolvePromise = resolve;
55
+ });
56
+ const mockFn = vi.fn(async () => promise);
57
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
58
+
59
+ act(() => {
60
+ result.current[1]();
61
+ });
62
+
63
+ // Should be pending immediately
64
+ expect(result.current[2]).toBe(true);
65
+
66
+ await act(async () => {
67
+ resolvePromise!('resolved');
68
+ await promise;
69
+ });
70
+
71
+ await waitFor(() => {
72
+ expect(result.current[2]).toBe(false);
73
+ });
74
+
75
+ expect(result.current[0]).toBe('resolved');
76
+ });
77
+
78
+ test('captures and stores errors', async () => {
79
+ const error = new Error('Test error');
80
+ const mockFn = vi.fn(async () => {
81
+ throw error;
82
+ });
83
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
84
+
85
+ await act(async () => {
86
+ result.current[1]();
87
+ });
88
+
89
+ await waitFor(() => {
90
+ expect(result.current[3]).toBe(error);
91
+ });
92
+
93
+ expect(result.current[0]).toBe('default'); // data remains unchanged
94
+ expect(result.current[2]).toBe(false); // isPending becomes false
95
+ });
96
+
97
+ test('does not update data if component unmounts before resolution', async () => {
98
+ let resolvePromise: (value: string) => void;
99
+ const promise = new Promise<string>((resolve) => {
100
+ resolvePromise = resolve;
101
+ });
102
+ const mockFn = vi.fn(async () => promise);
103
+ const { result, unmount } = renderHook(() => useFetch(mockFn, 'default'));
104
+
105
+ act(() => {
106
+ result.current[1]();
107
+ });
108
+
109
+ unmount();
110
+
111
+ await act(async () => {
112
+ resolvePromise!('resolved');
113
+ await promise;
114
+ });
115
+
116
+ // No assertions needed - just verifying no errors on unmount
117
+ expect(mockFn).toHaveBeenCalledTimes(1);
118
+ });
119
+
120
+ test('returns the promise from actionFn', async () => {
121
+ const mockFn = vi.fn(async () => 'result');
122
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
123
+
124
+ let returnedPromise: Promise<string>;
125
+ await act(async () => {
126
+ returnedPromise = result.current[1]();
127
+ });
128
+
129
+ await expect(returnedPromise!).resolves.toBe('result');
130
+ });
131
+
132
+ test('handles multiple sequential calls', async () => {
133
+ const mockFn = vi.fn(async (n: number) => `result-${n}`);
134
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
135
+
136
+ await act(async () => {
137
+ result.current[1](1);
138
+ });
139
+
140
+ await waitFor(() => {
141
+ expect(result.current[0]).toBe('result-1');
142
+ });
143
+
144
+ await act(async () => {
145
+ result.current[1](2);
146
+ });
147
+
148
+ await waitFor(() => {
149
+ expect(result.current[0]).toBe('result-2');
150
+ });
151
+
152
+ expect(mockFn).toHaveBeenCalledTimes(2);
153
+ });
154
+
155
+ test('preserves actionFn reference when fn dependency does not change', () => {
156
+ const mockFn = vi.fn(async () => 'result');
157
+ const { result, rerender } = renderHook(() => useFetch(mockFn, 'default'));
158
+
159
+ const firstActionFn = result.current[1];
160
+
161
+ rerender();
162
+
163
+ const secondActionFn = result.current[1];
164
+
165
+ expect(firstActionFn).toBe(secondActionFn);
166
+ });
167
+
168
+ test('updates actionFn when fn dependency changes', async () => {
169
+ const mockFn1 = vi.fn(async () => 'result1');
170
+ const mockFn2 = vi.fn(async () => 'result2');
171
+
172
+ const { result, rerender } = renderHook(({ fn }) => useFetch(fn, 'default'), {
173
+ initialProps: { fn: mockFn1 },
174
+ });
175
+
176
+ const firstActionFn = result.current[1];
177
+
178
+ await act(async () => {
179
+ result.current[1]();
180
+ });
181
+
182
+ await waitFor(() => {
183
+ expect(result.current[0]).toBe('result1');
184
+ });
185
+
186
+ // Change the function
187
+ rerender({ fn: mockFn2 });
188
+
189
+ const secondActionFn = result.current[1];
190
+
191
+ expect(firstActionFn).not.toBe(secondActionFn);
192
+
193
+ await act(async () => {
194
+ result.current[1]();
195
+ });
196
+
197
+ await waitFor(() => {
198
+ expect(result.current[0]).toBe('result2');
199
+ });
200
+
201
+ expect(mockFn1).toHaveBeenCalledTimes(1);
202
+ expect(mockFn2).toHaveBeenCalledTimes(1);
203
+ });
204
+
205
+ test('does not trigger execution on rerender', () => {
206
+ const mockFn = vi.fn(async () => 'result');
207
+ const { rerender } = renderHook(() => useFetch(mockFn, 'default'));
208
+
209
+ rerender();
210
+ rerender();
211
+ rerender();
212
+
213
+ expect(mockFn).not.toHaveBeenCalled();
214
+ });
215
+
216
+ test('handles concurrent calls correctly', async () => {
217
+ let callCount = 0;
218
+ const mockFn = vi.fn(async () => {
219
+ callCount++;
220
+ const currentCall = callCount;
221
+ await new Promise((resolve) => setTimeout(resolve, 10));
222
+ return `result-${currentCall}`;
223
+ });
224
+
225
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
226
+
227
+ // Fire multiple calls at once
228
+ await act(async () => {
229
+ result.current[1]();
230
+ result.current[1]();
231
+ result.current[1]();
232
+ });
233
+
234
+ await waitFor(() => {
235
+ expect(result.current[2]).toBe(false);
236
+ });
237
+
238
+ // Last call should win
239
+ expect(mockFn).toHaveBeenCalledTimes(3);
240
+ expect(['result-1', 'result-2', 'result-3']).toContain(result.current[0]);
241
+ });
242
+
243
+ test('maintains separate state for different hook instances', async () => {
244
+ const mockFn1 = vi.fn(async () => 'result1');
245
+ const mockFn2 = vi.fn(async () => 'result2');
246
+
247
+ const { result: result1 } = renderHook(() => useFetch(mockFn1, 'default1'));
248
+ const { result: result2 } = renderHook(() => useFetch(mockFn2, 'default2'));
249
+
250
+ expect(result1.current[0]).toBe('default1');
251
+ expect(result2.current[0]).toBe('default2');
252
+
253
+ await act(async () => {
254
+ result1.current[1]();
255
+ });
256
+
257
+ await waitFor(() => {
258
+ expect(result1.current[0]).toBe('result1');
259
+ });
260
+
261
+ expect(result2.current[0]).toBe('default2'); // unchanged
262
+
263
+ await act(async () => {
264
+ result2.current[1]();
265
+ });
266
+
267
+ await waitFor(() => {
268
+ expect(result2.current[0]).toBe('result2');
269
+ });
270
+
271
+ expect(result1.current[0]).toBe('result1'); // still unchanged
272
+ });
273
+
274
+ test('handles closure over external state', async () => {
275
+ let externalValue = 10;
276
+
277
+ const { result, rerender } = renderHook(() => {
278
+ const mockFn = async () => `value-${externalValue}`;
279
+ return useFetch(mockFn, 'default');
280
+ });
281
+
282
+ await act(async () => {
283
+ result.current[1]();
284
+ });
285
+
286
+ await waitFor(() => {
287
+ expect(result.current[0]).toBe('value-10');
288
+ });
289
+
290
+ // Change external value and rerender
291
+ externalValue = 20;
292
+ rerender();
293
+
294
+ await act(async () => {
295
+ result.current[1]();
296
+ });
297
+
298
+ await waitFor(() => {
299
+ expect(result.current[0]).toBe('value-20');
300
+ });
301
+ });
302
+
303
+ test('clears error on successful subsequent call', async () => {
304
+ const error = new Error('Test error');
305
+ let shouldError = true;
306
+
307
+ const mockFn = vi.fn(async () => {
308
+ if (shouldError) {
309
+ throw error;
310
+ }
311
+ return 'success';
312
+ });
313
+
314
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
315
+
316
+ // First call errors
317
+ await act(async () => {
318
+ result.current[1]();
319
+ });
320
+
321
+ await waitFor(() => {
322
+ expect(result.current[3]).toBe(error);
323
+ });
324
+
325
+ // Second call succeeds
326
+ shouldError = false;
327
+ await act(async () => {
328
+ result.current[1]();
329
+ });
330
+
331
+ await waitFor(() => {
332
+ expect(result.current[0]).toBe('success');
333
+ });
334
+
335
+ // Error is cleared automatically
336
+ expect(result.current[3]).toBe(undefined);
337
+ });
338
+
339
+ test('handles callback that returns a function with old value', async () => {
340
+ const mockFn = vi.fn((increment: number) => (oldValue: number) => oldValue + increment);
341
+ const { result } = renderHook(() => useFetch(mockFn, 0));
342
+
343
+ // First call - should call the function with oldValue (0) and get 0 + 5 = 5
344
+ await act(async () => {
345
+ result.current[1](5);
346
+ });
347
+
348
+ await waitFor(() => {
349
+ expect(result.current[0]).toBe(5);
350
+ });
351
+
352
+ // Second call - should call the function with oldValue (5) and get 5 + 10 = 15
353
+ await act(async () => {
354
+ result.current[1](10);
355
+ });
356
+
357
+ await waitFor(() => {
358
+ expect(result.current[0]).toBe(15);
359
+ });
360
+
361
+ expect(mockFn).toHaveBeenCalledTimes(2);
362
+ });
363
+
364
+ test('handles multiple calls with old value pattern', async () => {
365
+ const mockFn = vi.fn(
366
+ (operation: string, value: number) => (oldValue: string) => `${oldValue}-${operation}${value}`,
367
+ );
368
+ const { result } = renderHook(() => useFetch(mockFn, 'initial'));
369
+
370
+ // First call
371
+ await act(async () => {
372
+ result.current[1]('add', 1);
373
+ });
374
+
375
+ await waitFor(() => {
376
+ expect(result.current[0]).toBe('initial-add1');
377
+ });
378
+
379
+ // Second call - should use the updated value
380
+ await act(async () => {
381
+ result.current[1]('multiply', 2);
382
+ });
383
+
384
+ await waitFor(() => {
385
+ expect(result.current[0]).toBe('initial-add1-multiply2');
386
+ });
387
+
388
+ expect(mockFn).toHaveBeenCalledTimes(2);
389
+ });
390
+
391
+ test('handles counter pattern with old value', async () => {
392
+ const increment = vi.fn(() => (prev: number) => prev + 1);
393
+ const { result } = renderHook(() => useFetch(increment, 0));
394
+
395
+ // First increment: 0 + 1 = 1
396
+ await act(async () => {
397
+ result.current[1]();
398
+ });
399
+
400
+ await waitFor(() => {
401
+ expect(result.current[0]).toBe(1);
402
+ });
403
+
404
+ // Second increment: 1 + 1 = 2
405
+ await act(async () => {
406
+ result.current[1]();
407
+ });
408
+
409
+ await waitFor(() => {
410
+ expect(result.current[0]).toBe(2);
411
+ });
412
+
413
+ // Third increment: 2 + 1 = 3
414
+ await act(async () => {
415
+ result.current[1]();
416
+ });
417
+
418
+ await waitFor(() => {
419
+ expect(result.current[0]).toBe(3);
420
+ });
421
+
422
+ expect(increment).toHaveBeenCalledTimes(3);
423
+ });
424
+ });
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
  }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { useActionState, useCallback, useEffect, useState, startTransition } from 'react';
4
+
5
+ /**
6
+ * @param {(state: Awaited<R>, payload?: any) => Promise<R> | R} action
7
+ * @param {R} defaultValue
8
+ * @param {boolean} fetchOnMount
9
+ * @param {string} permalink
10
+ * @returns {[R, (payload?: P) => Promise<R> | R, boolean]}>
11
+ */
12
+ export function useFetchAction<S, P = undefined>(
13
+ action: (state: Awaited<S>, payload?: P) => S | Promise<S>,
14
+ defaultValue: Awaited<S>,
15
+ {
16
+ fetchOnMount = false,
17
+ permalink = undefined,
18
+ }: {
19
+ fetchOnMount?: boolean;
20
+ permalink?: string;
21
+ } = {},
22
+ ): [S, (payload: P) => void, boolean] {
23
+ const [data, dispatchAction, isPending] = useActionState<S, P | undefined>(action, defaultValue, permalink);
24
+ const [loading, setLoading] = useState(fetchOnMount);
25
+
26
+ const dispatch = useCallback(
27
+ (payload?: P) => {
28
+ startTransition(() => {
29
+ dispatchAction(payload);
30
+ setLoading(false);
31
+ });
32
+ },
33
+ [dispatchAction],
34
+ );
35
+
36
+ useEffect(() => {
37
+ if (!fetchOnMount || !loading) return;
38
+ dispatch(undefined);
39
+ }, [fetchOnMount, dispatch, loading]);
40
+
41
+ return [data, dispatch, isPending || loading];
42
+ }
@@ -1,10 +0,0 @@
1
- export declare function useFetcher<T = any>(cb: any, { fetchOnMount, onError }?: {
2
- fetchOnMount?: boolean;
3
- onError?: (e: Error) => void;
4
- }): {
5
- loading: boolean;
6
- error: Error | null;
7
- data: T | null;
8
- trigger: (...args: any[]) => Promise<any>;
9
- };
10
- //# sourceMappingURL=useFetcher.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useFetcher.d.ts","sourceRoot":"","sources":["../src/useFetcher.ts"],"names":[],"mappings":"AAEA,wBAAgB,UAAU,CAAC,CAAC,GAAG,GAAG,EAC9B,EAAE,EAAE,GAAG,EAEP,EAAE,YAAoB,EAAE,OAAO,EAAE,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;CAAO,GACjG;IACC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACf,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAC7C,CA0CA"}
@@ -1,43 +0,0 @@
1
- import { useState, useRef, useCallback, useEffect } from "react";
2
- function useFetcher(cb, { fetchOnMount = false, onError } = {}) {
3
- const [loading, setLoading] = useState(fetchOnMount);
4
- const [error, setError] = useState(null);
5
- const [data, setData] = useState(null);
6
- const isMounted = useRef(false);
7
- const trigger = useCallback(
8
- async (...args) => {
9
- try {
10
- setLoading(true);
11
- setError(null);
12
- const res = await cb(args);
13
- if (!isMounted.current) return;
14
- setData(res);
15
- return res;
16
- } catch (e) {
17
- console.error("Fetch failed", e);
18
- if (!isMounted.current) return;
19
- setError(e);
20
- onError?.(e);
21
- } finally {
22
- if (isMounted.current) setLoading(false);
23
- }
24
- },
25
- [cb, onError]
26
- );
27
- useEffect(() => {
28
- if (fetchOnMount) {
29
- trigger().catch((e) => console.error("Fetch on mount failed", e));
30
- }
31
- }, [fetchOnMount, cb, trigger]);
32
- useEffect(() => {
33
- isMounted.current = true;
34
- return () => {
35
- isMounted.current = false;
36
- };
37
- });
38
- return { loading, error, data, trigger };
39
- }
40
- export {
41
- useFetcher
42
- };
43
- //# sourceMappingURL=useFetcher.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useFetcher.js","sources":["../src/useFetcher.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport function useFetcher<T = any>(\n cb: any,\n\n { fetchOnMount = false, onError }: { fetchOnMount?: boolean; onError?: (e: Error) => void } = {},\n): {\n loading: boolean;\n error: Error | null;\n data: T | null;\n trigger: (...args: any[]) => Promise<any>;\n} {\n const [loading, setLoading] = useState(fetchOnMount);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<T | null>(null);\n\n const isMounted = useRef(false);\n\n const trigger = useCallback(\n async (...args: any[]): Promise<any> => {\n try {\n setLoading(true);\n setError(null);\n const res = await cb(args);\n if (!isMounted.current) return;\n setData(res);\n return res;\n } catch (e) {\n console.error('Fetch failed', e);\n if (!isMounted.current) return;\n setError(e);\n (onError as any)?.(e);\n } finally {\n if (isMounted.current) setLoading(false);\n }\n },\n [cb, onError],\n );\n\n useEffect(() => {\n if (fetchOnMount) {\n trigger().catch((e) => console.error('Fetch on mount failed', e)); // catch actually will never happen\n }\n }, [fetchOnMount, cb, trigger]);\n\n useEffect(() => {\n isMounted.current = true;\n return () => {\n isMounted.current = false;\n };\n });\n\n return { loading, error, data, trigger };\n}\n"],"names":[],"mappings":";AAEO,SAAS,WACZ,IAEA,EAAE,eAAe,OAAO,QAAA,IAAsE,IAMhG;AACE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,YAAY;AACnD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,IAAI;AAE/C,QAAM,YAAY,OAAO,KAAK;AAE9B,QAAM,UAAU;AAAA,IACZ,UAAU,SAA8B;AACpC,UAAI;AACA,mBAAW,IAAI;AACf,iBAAS,IAAI;AACb,cAAM,MAAM,MAAM,GAAG,IAAI;AACzB,YAAI,CAAC,UAAU,QAAS;AACxB,gBAAQ,GAAG;AACX,eAAO;AAAA,MACX,SAAS,GAAG;AACR,gBAAQ,MAAM,gBAAgB,CAAC;AAC/B,YAAI,CAAC,UAAU,QAAS;AACxB,iBAAS,CAAC;AACT,kBAAkB,CAAC;AAAA,MACxB,UAAA;AACI,YAAI,UAAU,QAAS,YAAW,KAAK;AAAA,MAC3C;AAAA,IACJ;AAAA,IACA,CAAC,IAAI,OAAO;AAAA,EAAA;AAGhB,YAAU,MAAM;AACZ,QAAI,cAAc;AACd,cAAA,EAAU,MAAM,CAAC,MAAM,QAAQ,MAAM,yBAAyB,CAAC,CAAC;AAAA,IACpE;AAAA,EACJ,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC;AAE9B,YAAU,MAAM;AACZ,cAAU,UAAU;AACpB,WAAO,MAAM;AACT,gBAAU,UAAU;AAAA,IACxB;AAAA,EACJ,CAAC;AAED,SAAO,EAAE,SAAS,OAAO,MAAM,QAAA;AACnC;"}
package/src/useFetcher.ts DELETED
@@ -1,54 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
-
3
- export function useFetcher<T = any>(
4
- cb: any,
5
-
6
- { fetchOnMount = false, onError }: { fetchOnMount?: boolean; onError?: (e: Error) => void } = {},
7
- ): {
8
- loading: boolean;
9
- error: Error | null;
10
- data: T | null;
11
- trigger: (...args: any[]) => Promise<any>;
12
- } {
13
- const [loading, setLoading] = useState(fetchOnMount);
14
- const [error, setError] = useState<Error | null>(null);
15
- const [data, setData] = useState<T | null>(null);
16
-
17
- const isMounted = useRef(false);
18
-
19
- const trigger = useCallback(
20
- async (...args: any[]): Promise<any> => {
21
- try {
22
- setLoading(true);
23
- setError(null);
24
- const res = await cb(args);
25
- if (!isMounted.current) return;
26
- setData(res);
27
- return res;
28
- } catch (e) {
29
- console.error('Fetch failed', e);
30
- if (!isMounted.current) return;
31
- setError(e);
32
- (onError as any)?.(e);
33
- } finally {
34
- if (isMounted.current) setLoading(false);
35
- }
36
- },
37
- [cb, onError],
38
- );
39
-
40
- useEffect(() => {
41
- if (fetchOnMount) {
42
- trigger().catch((e) => console.error('Fetch on mount failed', e)); // catch actually will never happen
43
- }
44
- }, [fetchOnMount, cb, trigger]);
45
-
46
- useEffect(() => {
47
- isMounted.current = true;
48
- return () => {
49
- isMounted.current = false;
50
- };
51
- });
52
-
53
- return { loading, error, data, trigger };
54
- }