@firtoz/router-toolkit 1.1.0 → 1.1.2
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/README.md +64 -9
- package/package.json +4 -5
- package/src/index.ts +1 -0
- package/src/types/RouteWithLoaderModule.ts +1 -1
- package/src/useCachedFetch.ts +5 -4
- package/src/useDynamicFetcher.ts +5 -4
- package/src/useDynamicSubmitter.tsx +7 -6
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ This package requires the following peer dependencies:
|
|
|
31
31
|
{
|
|
32
32
|
"react": "^18.0.0 || ^19.0.0",
|
|
33
33
|
"react-router": "^7.0.0",
|
|
34
|
-
"zod": "^4.0.
|
|
34
|
+
"zod": "^4.0.5"
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -86,21 +86,37 @@ function CachedComponent() {
|
|
|
86
86
|
|
|
87
87
|
Type-safe form submission with Zod validation and enhanced submit functionality.
|
|
88
88
|
|
|
89
|
+
**Basic Usage Pattern:**
|
|
90
|
+
|
|
89
91
|
```tsx
|
|
90
|
-
|
|
92
|
+
// app/routes/contact.tsx
|
|
93
|
+
import { useDynamicSubmitter, type RoutePath } from '@firtoz/router-toolkit';
|
|
91
94
|
import { z } from 'zod/v4';
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
// 1. Define your form schema
|
|
97
|
+
export const formSchema = z.object({
|
|
94
98
|
name: z.string(),
|
|
95
99
|
email: z.string().email(),
|
|
96
100
|
});
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
// 2. Export route constant
|
|
103
|
+
export const route: RoutePath<"contact"> = "contact";
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
// 3. Define your action
|
|
106
|
+
export const action = async ({ request }) => {
|
|
107
|
+
const formData = await request.formData();
|
|
108
|
+
// Handle submission
|
|
109
|
+
return { success: true };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// 4. Use the hook (requires full route module setup)
|
|
113
|
+
export default function ContactForm() {
|
|
114
|
+
// Note: This requires proper route module registration
|
|
115
|
+
const submitter = useDynamicSubmitter<{
|
|
116
|
+
file: "contact";
|
|
117
|
+
action: typeof action;
|
|
118
|
+
formSchema: typeof formSchema;
|
|
119
|
+
}>("contact");
|
|
104
120
|
|
|
105
121
|
return (
|
|
106
122
|
<submitter.Form method="POST">
|
|
@@ -112,6 +128,8 @@ function ContactForm() {
|
|
|
112
128
|
}
|
|
113
129
|
```
|
|
114
130
|
|
|
131
|
+
**Note:** `useDynamicSubmitter` requires advanced setup with route module registration and Zod schemas. For simpler use cases, you may prefer React Router's built-in `useFetcher`.
|
|
132
|
+
|
|
115
133
|
### `useFetcherStateChanged`
|
|
116
134
|
|
|
117
135
|
Track changes in fetcher state and react to them.
|
|
@@ -171,7 +189,44 @@ type ProfileArgs = HrefArgs<'/profile/:id'>;
|
|
|
171
189
|
|
|
172
190
|
## Usage with React Router 7 Framework Mode
|
|
173
191
|
|
|
174
|
-
This toolkit is specifically designed for React Router 7's framework mode.
|
|
192
|
+
This toolkit is specifically designed for React Router 7's framework mode. Here's the recommended pattern for setting up routes with router-toolkit:
|
|
193
|
+
|
|
194
|
+
### Route Setup Pattern
|
|
195
|
+
|
|
196
|
+
For each route file, follow this pattern to enable full type safety:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// app/routes/users.tsx
|
|
200
|
+
import { useDynamicFetcher, type RoutePath } from '@firtoz/router-toolkit';
|
|
201
|
+
|
|
202
|
+
// 1. Export your route constant with proper typing
|
|
203
|
+
export const route: RoutePath<"users"> = "users";
|
|
204
|
+
|
|
205
|
+
// 2. Define your loader/action as usual
|
|
206
|
+
export const loader = async () => {
|
|
207
|
+
return { users: [] }; // Your data
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// 3. Use the hook with typeof import for full type inference
|
|
211
|
+
export default function UsersPage() {
|
|
212
|
+
const fetcher = useDynamicFetcher<typeof import("./users")>("users");
|
|
213
|
+
|
|
214
|
+
const handleRefresh = () => {
|
|
215
|
+
fetcher.load(); // No need to specify URL - it's inferred
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div>
|
|
220
|
+
<button onClick={handleRefresh}>Refresh</button>
|
|
221
|
+
{fetcher.data && <div>{JSON.stringify(fetcher.data)}</div>}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Configuration
|
|
228
|
+
|
|
229
|
+
Make sure your routes are properly typed in your `react-router.config.ts`:
|
|
175
230
|
|
|
176
231
|
```tsx
|
|
177
232
|
// react-router.config.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/router-toolkit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"lint": "biome check src",
|
|
27
27
|
"format": "biome format src --write",
|
|
28
|
-
"test": "echo 'No tests yet'"
|
|
29
|
-
"semantic-release": "semantic-release -e semantic-release-monorepo"
|
|
28
|
+
"test": "echo 'No tests yet'"
|
|
30
29
|
},
|
|
31
30
|
"keywords": [
|
|
32
31
|
"react-router",
|
|
@@ -50,10 +49,10 @@
|
|
|
50
49
|
"url": "https://github.com/firtoz/router-toolkit/issues"
|
|
51
50
|
},
|
|
52
51
|
"peerDependencies": {
|
|
53
|
-
"@firtoz/maybe-error": "^1.
|
|
52
|
+
"@firtoz/maybe-error": "^1.2.1",
|
|
54
53
|
"react": "^19.1.0",
|
|
55
54
|
"react-router": "^7.6.3",
|
|
56
|
-
"zod": "^
|
|
55
|
+
"zod": "^4.0.5"
|
|
57
56
|
},
|
|
58
57
|
"engines": {
|
|
59
58
|
"node": ">=18.0.0"
|
package/src/index.ts
CHANGED
package/src/useCachedFetch.ts
CHANGED
|
@@ -8,10 +8,10 @@ const fetchCache = new Map<string, unknown>();
|
|
|
8
8
|
|
|
9
9
|
// Hook that uses regular fetch instead of useFetcher to avoid route invalidation
|
|
10
10
|
export const useCachedFetch = <TInfo extends RouteWithLoaderModule>(
|
|
11
|
-
path: TInfo["
|
|
12
|
-
...args: TInfo["
|
|
11
|
+
path: TInfo["route"],
|
|
12
|
+
...args: TInfo["route"] extends "undefined"
|
|
13
13
|
? HrefArgs<"/">
|
|
14
|
-
: HrefArgs<TInfo["
|
|
14
|
+
: HrefArgs<TInfo["route"]>
|
|
15
15
|
): {
|
|
16
16
|
data: ReturnType<typeof useLoaderData<TInfo["loader"]>> | undefined;
|
|
17
17
|
isLoading: boolean;
|
|
@@ -19,7 +19,8 @@ export const useCachedFetch = <TInfo extends RouteWithLoaderModule>(
|
|
|
19
19
|
} => {
|
|
20
20
|
// Generate URL using href, same as useDynamicFetcher
|
|
21
21
|
const url = useMemo(() => {
|
|
22
|
-
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: Typechecks complain about this so we need to cast to any
|
|
23
|
+
return (href as any)(path, ...args);
|
|
23
24
|
}, [path, args]);
|
|
24
25
|
|
|
25
26
|
// Use the generated URL as the cache key
|
package/src/useDynamicFetcher.ts
CHANGED
|
@@ -4,15 +4,16 @@ import type { HrefArgs } from "./types/HrefArgs";
|
|
|
4
4
|
import type { RouteWithLoaderModule } from "./types/RouteWithLoaderModule";
|
|
5
5
|
|
|
6
6
|
export const useDynamicFetcher = <TInfo extends RouteWithLoaderModule>(
|
|
7
|
-
path: TInfo["
|
|
8
|
-
...args: TInfo["
|
|
7
|
+
path: TInfo["route"],
|
|
8
|
+
...args: TInfo["route"] extends "undefined"
|
|
9
9
|
? HrefArgs<"/">
|
|
10
|
-
: HrefArgs<TInfo["
|
|
10
|
+
: HrefArgs<TInfo["route"]>
|
|
11
11
|
): Omit<ReturnType<typeof useFetcher<TInfo["loader"]>>, "load" | "submit"> & {
|
|
12
12
|
load: (queryParams?: Record<string, string>) => Promise<void>;
|
|
13
13
|
} => {
|
|
14
14
|
const url = useMemo(() => {
|
|
15
|
-
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: Typechecks complain about this so we need to cast to any
|
|
16
|
+
return (href as any)(path, ...args);
|
|
16
17
|
}, [path, args]);
|
|
17
18
|
|
|
18
19
|
const fetcher = useFetcher<TInfo["loader"]>({
|
|
@@ -12,9 +12,9 @@ import type { HrefArgs } from "./types/HrefArgs";
|
|
|
12
12
|
import type { RegisterPages } from "./types/RegisterPages";
|
|
13
13
|
|
|
14
14
|
type RouteModule = {
|
|
15
|
-
|
|
15
|
+
route: keyof RegisterPages;
|
|
16
16
|
action: Func;
|
|
17
|
-
formSchema: z.
|
|
17
|
+
formSchema: z.ZodType;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
type SubmitFunc<TModule extends RouteModule> = (
|
|
@@ -34,10 +34,10 @@ type SubmitForm = (
|
|
|
34
34
|
) => React.ReactElement;
|
|
35
35
|
|
|
36
36
|
export const useDynamicSubmitter = <TInfo extends RouteModule>(
|
|
37
|
-
path: TInfo["
|
|
38
|
-
...args: TInfo["
|
|
37
|
+
path: TInfo["route"],
|
|
38
|
+
...args: TInfo["route"] extends "undefined"
|
|
39
39
|
? HrefArgs<"/">
|
|
40
|
-
: HrefArgs<TInfo["
|
|
40
|
+
: HrefArgs<TInfo["route"]>
|
|
41
41
|
): Omit<
|
|
42
42
|
ReturnType<typeof useFetcher<TInfo["action"]>>,
|
|
43
43
|
"load" | "submit" | "Form"
|
|
@@ -46,7 +46,8 @@ export const useDynamicSubmitter = <TInfo extends RouteModule>(
|
|
|
46
46
|
Form: SubmitForm;
|
|
47
47
|
} => {
|
|
48
48
|
const url = useMemo(() => {
|
|
49
|
-
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: Typechecks complain about this so we need to cast to any
|
|
50
|
+
return (href as any)(path, ...args);
|
|
50
51
|
}, [path, args]);
|
|
51
52
|
|
|
52
53
|
const fetcher = useFetcher<TInfo["action"]>({
|