@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.1.5",
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,5 @@
1
+ export * from "./types/index";
2
+ export * from "./useCachedFetch";
3
+ export * from "./useDynamicSubmitter";
4
+ export * from "./useDynamicSubmitter";
5
+ export * from "./useFetcherStateChanged";
@@ -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,7 @@
1
+ import type { Func } from "./Func";
2
+ import type { RegisterPages } from "./RegisterPages";
3
+
4
+ export type RouteWithLoaderModule = {
5
+ file: keyof RegisterPages;
6
+ loader: Func;
7
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./HrefArgs";
2
+ export * from "./MaybeError";
3
+ export * from "./RoutePath";
@@ -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
+ };
@@ -1,15 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { href, useFetcher, type useLoaderData } from "react-router";
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 { RegisterPages } from "./types/RegisterPages";
4
+ import type { RouteWithLoaderModule } from "./types/RouteWithLoaderModule";
6
5
 
7
- type RouteModule = {
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
- // biome-ignore lint/suspicious/noExplicitAny: We are sure the args are correct
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"]>({