@firtoz/router-toolkit 1.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @firtoz/router-toolkit
2
2
 
3
- Type-safe React Router 7 framework mode helpers with enhanced fetching, form submission, and state management.
3
+ Type-safe React Router 7 framework mode helpers with enhanced fetching, form submission, and state management for React Router 7 framework mode.
4
4
 
5
5
  ## Features
6
6
 
@@ -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.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
- import { useDynamicSubmitter } from '@firtoz/router-toolkit';
92
+ // app/routes/contact.tsx
93
+ import { useDynamicSubmitter, type RoutePath } from '@firtoz/router-toolkit';
91
94
  import { z } from 'zod/v4';
92
95
 
93
- const formSchema = z.object({
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
- function ContactForm() {
99
- const submitter = useDynamicSubmitter('/api/contact');
102
+ // 2. Export route constant
103
+ export const route: RoutePath<"contact"> = "contact";
100
104
 
101
- const handleSubmit = (formData: z.infer<typeof formSchema>) => {
102
- submitter.submit(formData, { method: 'POST' });
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. Make sure your routes are properly typed in your `react-router.config.ts`:
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.0.0",
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,9 +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",
30
- "prepare": "husky"
28
+ "test": "echo 'No tests yet'"
31
29
  },
32
30
  "keywords": [
33
31
  "react-router",
@@ -44,31 +42,17 @@
44
42
  "homepage": "https://github.com/firtoz/router-toolkit#readme",
45
43
  "repository": {
46
44
  "type": "git",
47
- "url": "https://github.com/firtoz/router-toolkit.git"
45
+ "url": "https://github.com/firtoz/router-toolkit.git",
46
+ "directory": "packages/router-toolkit"
48
47
  },
49
48
  "bugs": {
50
49
  "url": "https://github.com/firtoz/router-toolkit/issues"
51
50
  },
52
51
  "peerDependencies": {
52
+ "@firtoz/maybe-error": "^1.2.1",
53
53
  "react": "^19.1.0",
54
54
  "react-router": "^7.6.3",
55
- "zod": "^3.25.69"
56
- },
57
- "devDependencies": {
58
- "@biomejs/biome": "^2.0.6",
59
- "@commitlint/cli": "^19.8.1",
60
- "@commitlint/config-conventional": "^19.8.1",
61
- "@semantic-release/changelog": "^6.0.3",
62
- "@semantic-release/git": "^10.0.1",
63
- "@semantic-release/github": "^11.0.3",
64
- "@semantic-release/npm": "^12.0.2",
65
- "@types/react": "^19.1.8",
66
- "husky": "^9.1.7",
67
- "react": "^19.1.0",
68
- "react-router": "^7.6.3",
69
- "semantic-release": "^24.2.7",
70
- "typescript": "^5.8.3",
71
- "zod": "^3.25.74"
55
+ "zod": "^4.0.5"
72
56
  },
73
57
  "engines": {
74
58
  "node": ">=18.0.0"
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./useCachedFetch";
3
3
  export * from "./useDynamicFetcher";
4
4
  export * from "./useDynamicSubmitter";
5
5
  export * from "./useFetcherStateChanged";
6
+ // Test comment to trigger release
@@ -2,6 +2,6 @@ import type { Func } from "./Func";
2
2
  import type { RegisterPages } from "./RegisterPages";
3
3
 
4
4
  export type RouteWithLoaderModule = {
5
- file: keyof RegisterPages;
5
+ route: keyof RegisterPages;
6
6
  loader: Func;
7
7
  };
@@ -1,3 +1,6 @@
1
+ export * from "@firtoz/maybe-error";
2
+ export * from "./Func";
1
3
  export * from "./HrefArgs";
2
- export * from "./MaybeError";
4
+ export * from "./RegisterPages";
3
5
  export * from "./RoutePath";
6
+ export * from "./RouteWithLoaderModule";
@@ -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["file"],
12
- ...args: TInfo["file"] extends "undefined"
11
+ path: TInfo["route"],
12
+ ...args: TInfo["route"] extends "undefined"
13
13
  ? HrefArgs<"/">
14
- : HrefArgs<TInfo["file"]>
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
- return href<typeof path>(path, ...args);
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
@@ -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["file"],
8
- ...args: TInfo["file"] extends "undefined"
7
+ path: TInfo["route"],
8
+ ...args: TInfo["route"] extends "undefined"
9
9
  ? HrefArgs<"/">
10
- : HrefArgs<TInfo["file"]>
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
- return href<typeof path>(path, ...args);
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
- file: keyof RegisterPages;
15
+ route: keyof RegisterPages;
16
16
  action: Func;
17
- formSchema: z.ZodTypeAny;
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["file"],
38
- ...args: TInfo["file"] extends "undefined"
37
+ path: TInfo["route"],
38
+ ...args: TInfo["route"] extends "undefined"
39
39
  ? HrefArgs<"/">
40
- : HrefArgs<TInfo["file"]>
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
- return href<typeof path>(path, ...args);
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"]>({
@@ -1,45 +0,0 @@
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
- };