@firtoz/router-toolkit 0.1.5 → 0.2.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/router-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Type-safe React Router 7 framework mode helpers with enhanced fetching, form submission, and state management",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type DefiniteError<TError = string> = {
|
|
2
|
+
success: false;
|
|
3
|
+
error: TError;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type DefiniteSuccess<T = undefined> = {
|
|
7
|
+
success: true;
|
|
8
|
+
} & (T extends undefined
|
|
9
|
+
? {
|
|
10
|
+
result?: T;
|
|
11
|
+
}
|
|
12
|
+
: {
|
|
13
|
+
result: T;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type MaybeError<T = undefined, TError = string> =
|
|
17
|
+
| DefiniteSuccess<T>
|
|
18
|
+
| DefiniteError<TError>;
|
|
19
|
+
|
|
20
|
+
export type AssumeSuccess<T extends MaybeError<unknown>> = Exclude<
|
|
21
|
+
T,
|
|
22
|
+
undefined
|
|
23
|
+
> extends MaybeError<infer U>
|
|
24
|
+
? U
|
|
25
|
+
: never;
|
|
26
|
+
|
|
27
|
+
export const success = <T = undefined>(
|
|
28
|
+
...params: T extends undefined ? [] : [T]
|
|
29
|
+
): DefiniteSuccess<T> => {
|
|
30
|
+
if (params.length === 0) {
|
|
31
|
+
return { success: true } as unknown as DefiniteSuccess<T>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
result: params[0],
|
|
37
|
+
} as unknown as DefiniteSuccess<T>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const fail = <TError = string>(error: TError): DefiniteError<TError> => {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { href, type useLoaderData } from "react-router";
|
|
3
|
+
import type { HrefArgs } from "./types/HrefArgs";
|
|
4
|
+
import type { RouteWithLoaderModule } from "./types/RouteWithLoaderModule";
|
|
5
|
+
|
|
6
|
+
// Cache for the useCachedFetch hook (regular fetch, not useFetcher)
|
|
7
|
+
const fetchCache = new Map<string, unknown>();
|
|
8
|
+
|
|
9
|
+
// Hook that uses regular fetch instead of useFetcher to avoid route invalidation
|
|
10
|
+
export const useCachedFetch = <TInfo extends RouteWithLoaderModule>(
|
|
11
|
+
path: TInfo["file"],
|
|
12
|
+
...args: TInfo["file"] extends "undefined"
|
|
13
|
+
? HrefArgs<"/">
|
|
14
|
+
: HrefArgs<TInfo["file"]>
|
|
15
|
+
): {
|
|
16
|
+
data: ReturnType<typeof useLoaderData<TInfo["loader"]>> | undefined;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
error: Error | undefined;
|
|
19
|
+
} => {
|
|
20
|
+
// Generate URL using href, same as useDynamicFetcher
|
|
21
|
+
const url = useMemo(() => {
|
|
22
|
+
return href<typeof path>(path, ...args);
|
|
23
|
+
}, [path, args]);
|
|
24
|
+
|
|
25
|
+
// Use the generated URL as the cache key
|
|
26
|
+
const cacheKey = url;
|
|
27
|
+
|
|
28
|
+
// Local state
|
|
29
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
30
|
+
const [error, setError] = useState<Error | undefined>(undefined);
|
|
31
|
+
const [data, setData] = useState<
|
|
32
|
+
ReturnType<typeof useLoaderData<TInfo["loader"]>> | undefined
|
|
33
|
+
>(() =>
|
|
34
|
+
fetchCache.has(cacheKey)
|
|
35
|
+
? (fetchCache.get(cacheKey) as ReturnType<
|
|
36
|
+
typeof useLoaderData<TInfo["loader"]>
|
|
37
|
+
>)
|
|
38
|
+
: undefined,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Auto-fetch on mount or when URL changes, if not in cache
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const fetchData = async () => {
|
|
44
|
+
// Skip fetch if data is already cached
|
|
45
|
+
if (fetchCache.has(cacheKey)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
setError(undefined);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(url);
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
|
|
61
|
+
// Update cache and state
|
|
62
|
+
fetchCache.set(cacheKey, result);
|
|
63
|
+
setData(result as ReturnType<typeof useLoaderData<TInfo["loader"]>>);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
66
|
+
} finally {
|
|
67
|
+
setIsLoading(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
fetchData();
|
|
72
|
+
}, [url, cacheKey]);
|
|
73
|
+
|
|
74
|
+
return { data, isLoading, error };
|
|
75
|
+
};
|
package/src/useDynamicFetcher.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import { href, useFetcher
|
|
3
|
-
import type { Func } from "./types/Func";
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { href, useFetcher } from "react-router";
|
|
4
3
|
import type { HrefArgs } from "./types/HrefArgs";
|
|
5
|
-
import type {
|
|
4
|
+
import type { RouteWithLoaderModule } from "./types/RouteWithLoaderModule";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
file: keyof RegisterPages;
|
|
9
|
-
loader: Func;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const useDynamicFetcher = <TInfo extends RouteModule>(
|
|
6
|
+
export const useDynamicFetcher = <TInfo extends RouteWithLoaderModule>(
|
|
13
7
|
path: TInfo["file"],
|
|
14
8
|
...args: TInfo["file"] extends "undefined"
|
|
15
9
|
? HrefArgs<"/">
|
|
@@ -47,75 +41,3 @@ export const useDynamicFetcher = <TInfo extends RouteModule>(
|
|
|
47
41
|
load,
|
|
48
42
|
};
|
|
49
43
|
};
|
|
50
|
-
|
|
51
|
-
// Cache for the useCachedFetch hook (regular fetch, not useFetcher)
|
|
52
|
-
const fetchCache = new Map<string, unknown>();
|
|
53
|
-
|
|
54
|
-
// Hook that uses regular fetch instead of useFetcher to avoid route invalidation
|
|
55
|
-
export const useCachedFetch = <TInfo extends RouteModule>(
|
|
56
|
-
path: TInfo["file"],
|
|
57
|
-
...args: TInfo["file"] extends "undefined"
|
|
58
|
-
? HrefArgs<"/">
|
|
59
|
-
: HrefArgs<TInfo["file"]>
|
|
60
|
-
): {
|
|
61
|
-
data: ReturnType<typeof useLoaderData<TInfo["loader"]>> | undefined;
|
|
62
|
-
isLoading: boolean;
|
|
63
|
-
error: Error | undefined;
|
|
64
|
-
} => {
|
|
65
|
-
// Generate URL using href, same as useDynamicFetcher
|
|
66
|
-
const url = useMemo(() => {
|
|
67
|
-
// biome-ignore lint/suspicious/noExplicitAny: Complex conditional typing prevents TypeScript from inferring args when spreading
|
|
68
|
-
return href<typeof path>(path, ...(args as any));
|
|
69
|
-
}, [path, args]);
|
|
70
|
-
|
|
71
|
-
// Use the generated URL as the cache key
|
|
72
|
-
const cacheKey = url;
|
|
73
|
-
|
|
74
|
-
// Local state
|
|
75
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
76
|
-
const [error, setError] = useState<Error | undefined>(undefined);
|
|
77
|
-
const [data, setData] = useState<
|
|
78
|
-
ReturnType<typeof useLoaderData<TInfo["loader"]>> | undefined
|
|
79
|
-
>(() =>
|
|
80
|
-
fetchCache.has(cacheKey)
|
|
81
|
-
? (fetchCache.get(cacheKey) as ReturnType<
|
|
82
|
-
typeof useLoaderData<TInfo["loader"]>
|
|
83
|
-
>)
|
|
84
|
-
: undefined,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
// Auto-fetch on mount or when URL changes, if not in cache
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
const fetchData = async () => {
|
|
90
|
-
// Skip fetch if data is already cached
|
|
91
|
-
if (fetchCache.has(cacheKey)) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
setIsLoading(true);
|
|
96
|
-
setError(undefined);
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const response = await fetch(url);
|
|
100
|
-
|
|
101
|
-
if (!response.ok) {
|
|
102
|
-
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const result = await response.json();
|
|
106
|
-
|
|
107
|
-
// Update cache and state
|
|
108
|
-
fetchCache.set(cacheKey, result);
|
|
109
|
-
setData(result as ReturnType<typeof useLoaderData<TInfo["loader"]>>);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
112
|
-
} finally {
|
|
113
|
-
setIsLoading(false);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
fetchData();
|
|
118
|
-
}, [url, cacheKey]);
|
|
119
|
-
|
|
120
|
-
return { data, isLoading, error };
|
|
121
|
-
};
|
|
@@ -46,8 +46,7 @@ export const useDynamicSubmitter = <TInfo extends RouteModule>(
|
|
|
46
46
|
Form: SubmitForm;
|
|
47
47
|
} => {
|
|
48
48
|
const url = useMemo(() => {
|
|
49
|
-
|
|
50
|
-
return href<typeof path>(path, ...(args as any));
|
|
49
|
+
return href<typeof path>(path, ...args);
|
|
51
50
|
}, [path, args]);
|
|
52
51
|
|
|
53
52
|
const fetcher = useFetcher<TInfo["action"]>({
|