@firtoz/router-toolkit 7.0.2 → 8.0.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.
Files changed (75) hide show
  1. package/README.md +5 -1
  2. package/dist/ConcurrentSubmitterProvider.d.ts +41 -0
  3. package/dist/ConcurrentSubmitterProvider.js +3 -0
  4. package/dist/ConcurrentSubmitterProvider.js.map +1 -0
  5. package/dist/chunk-2RLEUOSR.js +3 -0
  6. package/dist/chunk-2RLEUOSR.js.map +1 -0
  7. package/dist/chunk-2W5QFQTE.js +3 -0
  8. package/dist/chunk-2W5QFQTE.js.map +1 -0
  9. package/dist/chunk-5MOCOBGV.js +45 -0
  10. package/dist/chunk-5MOCOBGV.js.map +1 -0
  11. package/dist/chunk-HX57TC2S.js +33 -0
  12. package/dist/chunk-HX57TC2S.js.map +1 -0
  13. package/dist/chunk-JJN6GBJL.js +55 -0
  14. package/dist/chunk-JJN6GBJL.js.map +1 -0
  15. package/dist/chunk-MKH4ZP5U.js +3 -0
  16. package/dist/chunk-MKH4ZP5U.js.map +1 -0
  17. package/dist/chunk-OQGTU34H.js +44 -0
  18. package/dist/chunk-OQGTU34H.js.map +1 -0
  19. package/dist/chunk-QZDEBX3D.js +71 -0
  20. package/dist/chunk-QZDEBX3D.js.map +1 -0
  21. package/dist/chunk-R6HEJ5E5.js +3 -0
  22. package/dist/chunk-R6HEJ5E5.js.map +1 -0
  23. package/dist/chunk-S4QCJFVR.js +3 -0
  24. package/dist/chunk-S4QCJFVR.js.map +1 -0
  25. package/dist/chunk-W5R7YYHR.js +16 -0
  26. package/dist/chunk-W5R7YYHR.js.map +1 -0
  27. package/dist/chunk-YYJDSKJG.js +3 -0
  28. package/dist/chunk-YYJDSKJG.js.map +1 -0
  29. package/dist/chunk-ZC6U3MIB.js +189 -0
  30. package/dist/chunk-ZC6U3MIB.js.map +1 -0
  31. package/dist/chunk-ZMRGBYFH.js +3 -0
  32. package/dist/chunk-ZMRGBYFH.js.map +1 -0
  33. package/dist/formAction.d.ts +290 -0
  34. package/dist/formAction.js +3 -0
  35. package/dist/formAction.js.map +1 -0
  36. package/dist/index.d.ts +18 -0
  37. package/dist/index.js +16 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/types/Func.d.ts +3 -0
  40. package/dist/types/Func.js +3 -0
  41. package/dist/types/Func.js.map +1 -0
  42. package/dist/types/HrefArgs.d.ts +8 -0
  43. package/dist/types/HrefArgs.js +3 -0
  44. package/dist/types/HrefArgs.js.map +1 -0
  45. package/dist/types/RegisterPages.d.ts +11 -0
  46. package/dist/types/RegisterPages.js +3 -0
  47. package/dist/types/RegisterPages.js.map +1 -0
  48. package/dist/types/RoutePath.d.ts +6 -0
  49. package/dist/types/RoutePath.js +3 -0
  50. package/dist/types/RoutePath.js.map +1 -0
  51. package/dist/types/RouteWithActionModule.d.ts +18 -0
  52. package/dist/types/RouteWithActionModule.js +3 -0
  53. package/dist/types/RouteWithActionModule.js.map +1 -0
  54. package/dist/types/RouteWithLoaderModule.d.ts +10 -0
  55. package/dist/types/RouteWithLoaderModule.js +3 -0
  56. package/dist/types/RouteWithLoaderModule.js.map +1 -0
  57. package/dist/types/index.d.ts +9 -0
  58. package/dist/types/index.js +9 -0
  59. package/dist/types/index.js.map +1 -0
  60. package/dist/useCachedFetch.d.ts +13 -0
  61. package/dist/useCachedFetch.js +3 -0
  62. package/dist/useCachedFetch.js.map +1 -0
  63. package/dist/useConcurrentSubmitter.d.ts +34 -0
  64. package/dist/useConcurrentSubmitter.js +4 -0
  65. package/dist/useConcurrentSubmitter.js.map +1 -0
  66. package/dist/useDynamicFetcher.d.ts +219 -0
  67. package/dist/useDynamicFetcher.js +3 -0
  68. package/dist/useDynamicFetcher.js.map +1 -0
  69. package/dist/useDynamicSubmitter.d.ts +256 -0
  70. package/dist/useDynamicSubmitter.js +3 -0
  71. package/dist/useDynamicSubmitter.js.map +1 -0
  72. package/dist/useFetcherStateChanged.d.ts +10 -0
  73. package/dist/useFetcherStateChanged.js +3 -0
  74. package/dist/useFetcherStateChanged.js.map +1 -0
  75. package/package.json +21 -12
@@ -0,0 +1,10 @@
1
+ import { Func } from './Func.js';
2
+ import { RegisterPages } from './RegisterPages.js';
3
+ import 'react-router';
4
+
5
+ type RouteWithLoaderModule = {
6
+ route: keyof RegisterPages;
7
+ loader: Func;
8
+ };
9
+
10
+ export type { RouteWithLoaderModule };
@@ -0,0 +1,3 @@
1
+ import '../chunk-2W5QFQTE.js';
2
+ //# sourceMappingURL=RouteWithLoaderModule.js.map
3
+ //# sourceMappingURL=RouteWithLoaderModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"RouteWithLoaderModule.js"}
@@ -0,0 +1,9 @@
1
+ export * from '@firtoz/maybe-error';
2
+ export { Func } from './Func.js';
3
+ export { HrefArgs } from './HrefArgs.js';
4
+ export { RegisterPages } from './RegisterPages.js';
5
+ export { RoutePath } from './RoutePath.js';
6
+ export { ActionResult, RouteWithActionModule } from './RouteWithActionModule.js';
7
+ export { RouteWithLoaderModule } from './RouteWithLoaderModule.js';
8
+ import 'react-router';
9
+ import 'zod';
@@ -0,0 +1,9 @@
1
+ import '../chunk-2RLEUOSR.js';
2
+ import '../chunk-2W5QFQTE.js';
3
+ import '../chunk-YYJDSKJG.js';
4
+ import '../chunk-ZMRGBYFH.js';
5
+ import '../chunk-MKH4ZP5U.js';
6
+ import '../chunk-S4QCJFVR.js';
7
+ import '../chunk-R6HEJ5E5.js';
8
+ //# sourceMappingURL=index.js.map
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,13 @@
1
+ import { useLoaderData } from 'react-router';
2
+ import { HrefArgs } from './types/HrefArgs.js';
3
+ import { RouteWithLoaderModule } from './types/RouteWithLoaderModule.js';
4
+ import './types/RegisterPages.js';
5
+ import './types/Func.js';
6
+
7
+ declare const useCachedFetch: <TInfo extends RouteWithLoaderModule>(path: TInfo["route"], ...args: TInfo["route"] extends "undefined" ? HrefArgs<"/"> : HrefArgs<TInfo["route"]>) => {
8
+ data: ReturnType<typeof useLoaderData<TInfo["loader"]>> | undefined;
9
+ isLoading: boolean;
10
+ error: Error | undefined;
11
+ };
12
+
13
+ export { useCachedFetch };
@@ -0,0 +1,3 @@
1
+ export { useCachedFetch } from './chunk-OQGTU34H.js';
2
+ //# sourceMappingURL=useCachedFetch.js.map
3
+ //# sourceMappingURL=useCachedFetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"useCachedFetch.js"}
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import { RegisterPages } from './types/RegisterPages.js';
3
+ import { RouteWithActionModule, ActionResult } from './types/RouteWithActionModule.js';
4
+ import { Operation, FormDataSubmittedData, SubmitJsonOptions, SubmitJsonResult, SubmitFormDataOptions } from './ConcurrentSubmitterProvider.js';
5
+ import 'react-router';
6
+ import './types/Func.js';
7
+ import 'react/jsx-runtime';
8
+ import 'react';
9
+
10
+ /**
11
+ * @fileoverview Typed hook for concurrent form submissions. Use within ConcurrentSubmitterProvider.
12
+ */
13
+
14
+ type RouteParams<R extends keyof RegisterPages> = RegisterPages[R]["params"];
15
+ /**
16
+ * True when the route has no dynamic segments (`params: {}` from React Router typegen).
17
+ * Uses `keyof … extends never` so real empty params stay distinct from the `RegisterPages`
18
+ * fallback (`AnyPages` → `params: Record<string, …>` has `keyof` = `string`, not `never`).
19
+ * That avoids mis-resolving `submitJson(path, data)` as the route-args overload (strings only).
20
+ */
21
+ type HasNoParams<R extends keyof RegisterPages> = [
22
+ keyof RegisterPages[R]["params"]
23
+ ] extends [never] ? true : false;
24
+ type HasOptionalParams<R extends keyof RegisterPages> = HasNoParams<R> extends true ? false : Partial<RouteParams<R>> extends RouteParams<R> ? true : false;
25
+ type SubmitJsonFn<TInfo extends RouteWithActionModule> = HasNoParams<TInfo["route"]> extends true ? (path: TInfo["route"], data: z.infer<TInfo["formSchema"]>, options?: SubmitJsonOptions) => SubmitJsonResult<ActionResult<TInfo>> : HasOptionalParams<TInfo["route"]> extends true ? (path: TInfo["route"], args: RouteParams<TInfo["route"]> | undefined, data: z.infer<TInfo["formSchema"]>, options?: SubmitJsonOptions) => SubmitJsonResult<ActionResult<TInfo>> : (path: TInfo["route"], args: RouteParams<TInfo["route"]>, data: z.infer<TInfo["formSchema"]>, options?: SubmitJsonOptions) => SubmitJsonResult<ActionResult<TInfo>>;
26
+ type SubmitFormDataFn<TInfo extends RouteWithActionModule> = HasNoParams<TInfo["route"]> extends true ? (path: TInfo["route"], formData: FormData, submittedData?: FormDataSubmittedData, options?: SubmitFormDataOptions) => SubmitJsonResult<ActionResult<TInfo>> : HasOptionalParams<TInfo["route"]> extends true ? (path: TInfo["route"], args: RouteParams<TInfo["route"]> | undefined, formData: FormData, submittedData?: FormDataSubmittedData, options?: SubmitFormDataOptions) => SubmitJsonResult<ActionResult<TInfo>> : (path: TInfo["route"], args: RouteParams<TInfo["route"]>, formData: FormData, submittedData?: FormDataSubmittedData, options?: SubmitFormDataOptions) => SubmitJsonResult<ActionResult<TInfo>>;
27
+ type UseConcurrentSubmitterReturn<TInfo extends RouteWithActionModule> = {
28
+ operations: Record<string, Operation<ActionResult<TInfo>, z.infer<TInfo["formSchema"]> | FormDataSubmittedData>>;
29
+ submitJson: SubmitJsonFn<TInfo>;
30
+ submitFormData: SubmitFormDataFn<TInfo>;
31
+ };
32
+ declare function useConcurrentSubmitter<TInfo extends RouteWithActionModule>(): UseConcurrentSubmitterReturn<TInfo>;
33
+
34
+ export { ActionResult, type UseConcurrentSubmitterReturn, useConcurrentSubmitter };
@@ -0,0 +1,4 @@
1
+ export { useConcurrentSubmitter } from './chunk-QZDEBX3D.js';
2
+ import './chunk-ZC6U3MIB.js';
3
+ //# sourceMappingURL=useConcurrentSubmitter.js.map
4
+ //# sourceMappingURL=useConcurrentSubmitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"useConcurrentSubmitter.js"}
@@ -0,0 +1,219 @@
1
+ import { useFetcher } from 'react-router';
2
+ import { HrefArgs } from './types/HrefArgs.js';
3
+ import { RouteWithLoaderModule } from './types/RouteWithLoaderModule.js';
4
+ import './types/RegisterPages.js';
5
+ import './types/Func.js';
6
+
7
+ /**
8
+ * @fileoverview Type-safe dynamic data fetching hook for React Router 7
9
+ *
10
+ * This module provides a hook that creates a type-safe fetcher for loading data
11
+ * from dynamic routes with full TypeScript inference for the loader response and route params.
12
+ *
13
+ * @example
14
+ * ### Route Setup (`app/routes/api.users.$userId.ts`)
15
+ *
16
+ * First, set up your route with the required exports:
17
+ *
18
+ * ```typescript
19
+ * import type { RoutePath } from "@firtoz/router-toolkit";
20
+ *
21
+ * // Export the route path for type inference
22
+ * export const route: RoutePath<"/api/users/:userId"> = "/api/users/:userId";
23
+ *
24
+ * // Define the loader with a typed return value
25
+ * export const loader = async ({ params }: LoaderFunctionArgs) => {
26
+ * const user = await db.users.findUnique({ where: { id: params.userId } });
27
+ * return {
28
+ * user: {
29
+ * id: user.id,
30
+ * email: user.email,
31
+ * displayName: user.displayName,
32
+ * createdAt: user.createdAt.toISOString(),
33
+ * },
34
+ * };
35
+ * };
36
+ * ```
37
+ *
38
+ * @example
39
+ * ### Using the hook in a component
40
+ *
41
+ * ```tsx
42
+ * import { useDynamicFetcher } from "@firtoz/router-toolkit";
43
+ * import { useEffect } from "react";
44
+ *
45
+ * function UserProfile({ userId }: { userId: string }) {
46
+ * // Type-safe fetcher with full inference
47
+ * const fetcher = useDynamicFetcher<typeof import("./api.users.$userId")>(
48
+ * "/api/users/:userId",
49
+ * { userId }
50
+ * );
51
+ *
52
+ * // Load data on mount
53
+ * useEffect(() => {
54
+ * fetcher.load();
55
+ * }, [fetcher.load]);
56
+ *
57
+ * // fetcher.data is fully typed: { user: { id, email, displayName, createdAt } } | undefined
58
+ * if (fetcher.state === "loading") {
59
+ * return <div>Loading...</div>;
60
+ * }
61
+ *
62
+ * if (!fetcher.data) {
63
+ * return <div>No user found</div>;
64
+ * }
65
+ *
66
+ * return (
67
+ * <div>
68
+ * <h1>{fetcher.data.user.displayName}</h1>
69
+ * <p>{fetcher.data.user.email}</p>
70
+ * </div>
71
+ * );
72
+ * }
73
+ * ```
74
+ *
75
+ * @example
76
+ * ### Loading with query parameters
77
+ *
78
+ * ```tsx
79
+ * function SearchResults() {
80
+ * const fetcher = useDynamicFetcher<typeof import("./api.search")>("/api/search");
81
+ *
82
+ * const handleSearch = (query: string, page: number) => {
83
+ * // Pass query params to the load function
84
+ * fetcher.load({ q: query, page: String(page) });
85
+ * };
86
+ *
87
+ * return (
88
+ * <div>
89
+ * <input onChange={(e) => handleSearch(e.target.value, 1)} />
90
+ * {fetcher.data?.results.map((result) => (
91
+ * <div key={result.id}>{result.title}</div>
92
+ * ))}
93
+ * </div>
94
+ * );
95
+ * }
96
+ * ```
97
+ *
98
+ * @example
99
+ * ### Combining with useDynamicSubmitter for full CRUD
100
+ *
101
+ * You can use `useDynamicFetcher` alongside `useDynamicSubmitter` to create
102
+ * complete CRUD interfaces with type safety:
103
+ *
104
+ * ```tsx
105
+ * import { useDynamicFetcher, useDynamicSubmitter } from "@firtoz/router-toolkit";
106
+ *
107
+ * function PostEditor({ postId }: { postId: string }) {
108
+ * // Fetch post data
109
+ * const fetcher = useDynamicFetcher<typeof import("./api.posts.$postId")>(
110
+ * "/api/posts/:postId",
111
+ * { postId }
112
+ * );
113
+ *
114
+ * // Submit updates
115
+ * const submitter = useDynamicSubmitter<typeof import("./api.posts.$postId")>(
116
+ * "/api/posts/:postId",
117
+ * { postId }
118
+ * );
119
+ *
120
+ * useEffect(() => {
121
+ * fetcher.load();
122
+ * }, [fetcher.load]);
123
+ *
124
+ * const handleSave = async (title: string, content: string) => {
125
+ * await submitter.submitJson({ title, content }, { method: "PUT" });
126
+ * // Reload after save
127
+ * fetcher.load();
128
+ * };
129
+ *
130
+ * if (!fetcher.data) return <div>Loading...</div>;
131
+ *
132
+ * return (
133
+ * <form onSubmit={(e) => {
134
+ * e.preventDefault();
135
+ * const form = new FormData(e.currentTarget);
136
+ * handleSave(form.get("title") as string, form.get("content") as string);
137
+ * }}>
138
+ * <input name="title" defaultValue={fetcher.data.post.title} />
139
+ * <textarea name="content" defaultValue={fetcher.data.post.content} />
140
+ * <button disabled={submitter.state !== "idle"}>
141
+ * {submitter.state === "submitting" ? "Saving..." : "Save"}
142
+ * </button>
143
+ * </form>
144
+ * );
145
+ * }
146
+ * ```
147
+ */
148
+
149
+ /**
150
+ * Creates a type-safe fetcher for loading data from dynamic routes.
151
+ *
152
+ * This hook provides full TypeScript inference for:
153
+ * - Route parameters (from the route path)
154
+ * - Loader response type (from the route's loader export)
155
+ *
156
+ * @template TInfo - The route module type (use `typeof import("./route-file")`)
157
+ *
158
+ * @param path - The route path (must match the route's `route` export)
159
+ * @param args - Route parameters (if the route has dynamic segments like `:id`)
160
+ *
161
+ * @returns An extended fetcher object with:
162
+ * - `load` - Function to load data, optionally with query parameters
163
+ * - `data` - Response data from the loader (typed)
164
+ * - `state` - Fetcher state ("idle" | "loading" | "submitting")
165
+ * - All other useFetcher properties (except `submit`)
166
+ *
167
+ * @example
168
+ * ### Basic usage
169
+ *
170
+ * ```typescript
171
+ * // In your route file (app/routes/api.products.$productId.ts):
172
+ * export const route: RoutePath<"/api/products/:productId"> = "/api/products/:productId";
173
+ * export const loader = async ({ params }: LoaderFunctionArgs) => {
174
+ * return { product: await getProduct(params.productId) };
175
+ * };
176
+ *
177
+ * // In your component:
178
+ * const fetcher = useDynamicFetcher<typeof import("./api.products.$productId")>(
179
+ * "/api/products/:productId",
180
+ * { productId: "abc123" }
181
+ * );
182
+ *
183
+ * useEffect(() => {
184
+ * fetcher.load();
185
+ * }, [fetcher.load]);
186
+ *
187
+ * // fetcher.data is typed as { product: Product } | undefined
188
+ * ```
189
+ *
190
+ * @example
191
+ * ### With query parameters
192
+ *
193
+ * ```typescript
194
+ * const fetcher = useDynamicFetcher<typeof import("./api.search")>("/api/search");
195
+ *
196
+ * // Load with query params: /api/search?q=hello&limit=10
197
+ * fetcher.load({ q: "hello", limit: "10" });
198
+ * ```
199
+ */
200
+ declare const useDynamicFetcher: <TInfo extends RouteWithLoaderModule>(path: TInfo["route"], ...args: TInfo["route"] extends "undefined" ? HrefArgs<"/"> : HrefArgs<TInfo["route"]>) => Omit<ReturnType<typeof useFetcher<TInfo["loader"]>>, "load" | "submit"> & {
201
+ /**
202
+ * Load data from the route's loader.
203
+ *
204
+ * @param queryParams - Optional query parameters to append to the URL
205
+ * @returns A promise that resolves when the load is complete
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * // Load without query params
210
+ * fetcher.load();
211
+ *
212
+ * // Load with query params
213
+ * fetcher.load({ page: "2", sort: "name" });
214
+ * ```
215
+ */
216
+ load: (queryParams?: Record<string, string>) => Promise<void>;
217
+ };
218
+
219
+ export { useDynamicFetcher };
@@ -0,0 +1,3 @@
1
+ export { useDynamicFetcher } from './chunk-HX57TC2S.js';
2
+ //# sourceMappingURL=useDynamicFetcher.js.map
3
+ //# sourceMappingURL=useDynamicFetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"useDynamicFetcher.js"}
@@ -0,0 +1,256 @@
1
+ import React from 'react';
2
+ import { useFetcher, SubmitTarget, SubmitOptions, FetcherFormProps } from 'react-router';
3
+ import { z } from 'zod';
4
+ import { HrefArgs } from './types/HrefArgs.js';
5
+ import { RouteWithActionModule } from './types/RouteWithActionModule.js';
6
+ import './types/RegisterPages.js';
7
+ import './types/Func.js';
8
+
9
+ /**
10
+ * @fileoverview Type-safe dynamic form submission hook for React Router 7
11
+ *
12
+ * This module provides a hook that creates a type-safe fetcher for submitting forms
13
+ * to dynamic routes with full TypeScript inference for the form schema and route params.
14
+ *
15
+ * @example
16
+ * ### Route Setup (`app/routes/admin.posts.$id.tsx`)
17
+ *
18
+ * First, set up your route with the required exports:
19
+ *
20
+ * ```typescript
21
+ * import { z } from "zod";
22
+ * import { formAction, type RoutePath } from "@firtoz/router-toolkit";
23
+ * import { success, fail } from "@firtoz/maybe-error";
24
+ *
25
+ * // Export the route path for type inference
26
+ * export const route: RoutePath<"/admin/posts/:id"> = "/admin/posts/:id";
27
+ *
28
+ * // Define the form schema
29
+ * export const formSchema = z.object({
30
+ * title: z.string().min(1, "Title is required"),
31
+ * content: z.string().min(10, "Content must be at least 10 characters"),
32
+ * published: z.boolean().optional().default(false),
33
+ * });
34
+ *
35
+ * // Create the action using formAction
36
+ * export const action = formAction({
37
+ * schema: formSchema,
38
+ * handler: async ({ request, params }, formData) => {
39
+ * const postId = params.id;
40
+ * const updated = await db.posts.update({
41
+ * where: { id: postId },
42
+ * data: formData,
43
+ * });
44
+ * return success(updated);
45
+ * },
46
+ * });
47
+ * ```
48
+ *
49
+ * @example
50
+ * ### Using the hook in a component
51
+ *
52
+ * ```tsx
53
+ * import { useDynamicSubmitter } from "@firtoz/router-toolkit";
54
+ *
55
+ * function EditPostForm({ postId }: { postId: string }) {
56
+ * // Type-safe submitter with full inference
57
+ * const submitter = useDynamicSubmitter<typeof import("./admin.posts.$id")>(
58
+ * "/admin/posts/:id",
59
+ * { id: postId }
60
+ * );
61
+ *
62
+ * // submitter.data is the typed response from the action
63
+ * // submitter.state is "idle" | "loading" | "submitting"
64
+ *
65
+ * // Option 1: Submit as JSON (recommended for programmatic submissions)
66
+ * // Defaults to POST if no options provided
67
+ * const handleSubmitJson = async () => {
68
+ * await submitter.submitJson({
69
+ * title: "My Post",
70
+ * content: "Post content here",
71
+ * published: true,
72
+ * });
73
+ * };
74
+ *
75
+ * // Option 2: Submit with FormData or SubmitTarget
76
+ * const handleSubmit = async (formData: FormData) => {
77
+ * await submitter.submit(formData, { method: "POST" });
78
+ * };
79
+ *
80
+ * // Option 3: Use the Form component (defaults to POST)
81
+ * return (
82
+ * <submitter.Form>
83
+ * <input name="title" />
84
+ * <textarea name="content" />
85
+ * <button type="submit">Save</button>
86
+ * </submitter.Form>
87
+ * );
88
+ * }
89
+ * ```
90
+ *
91
+ * @example
92
+ * ### Handling responses
93
+ *
94
+ * ```tsx
95
+ * function LoginForm() {
96
+ * const submitter = useDynamicSubmitter<typeof import("./auth.login")>("/auth/login");
97
+ *
98
+ * useEffect(() => {
99
+ * if (submitter.data?.success) {
100
+ * // Handle success
101
+ * console.log("Logged in as:", submitter.data.value.user.email);
102
+ * } else if (submitter.data && !submitter.data.success) {
103
+ * // Handle error
104
+ * if (submitter.data.error.type === "validation") {
105
+ * console.log("Validation errors:", submitter.data.error.error);
106
+ * }
107
+ * }
108
+ * }, [submitter.data]);
109
+ *
110
+ * return (
111
+ * <submitter.Form>
112
+ * <input name="email" type="email" />
113
+ * <input name="password" type="password" />
114
+ * <button disabled={submitter.state !== "idle"}>
115
+ * {submitter.state === "submitting" ? "Logging in..." : "Login"}
116
+ * </button>
117
+ * </submitter.Form>
118
+ * );
119
+ * }
120
+ * ```
121
+ */
122
+
123
+ /**
124
+ * Function type for submitting form data with a SubmitTarget.
125
+ *
126
+ * Accepts the form schema data combined with SubmitTarget (FormData, HTMLFormElement, etc.)
127
+ * Use this when you have a FormData object or form element reference.
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * // With FormData
132
+ * submitter.submit(formData, { method: "POST" });
133
+ *
134
+ * // With form element reference
135
+ * submitter.submit(formRef.current, { method: "POST" });
136
+ * ```
137
+ */
138
+ type SubmitFunc<TModule extends RouteWithActionModule> = (target: z.infer<TModule["formSchema"]> & SubmitTarget, options: Omit<SubmitOptions, "action" | "method" | "encType"> & {
139
+ method: Exclude<SubmitOptions["method"], "GET">;
140
+ }) => Promise<void>;
141
+ /**
142
+ * Options for submitJson function.
143
+ * Method defaults to "POST" if not specified.
144
+ */
145
+ type SubmitJsonOptions = Omit<SubmitOptions, "action" | "method" | "encType"> & {
146
+ method?: Exclude<SubmitOptions["method"], "GET">;
147
+ };
148
+ /**
149
+ * Function type for submitting form data as JSON.
150
+ *
151
+ * Accepts only the inferred form schema type (plain object).
152
+ * Automatically serializes the data as JSON. This is the recommended
153
+ * approach for programmatic form submissions.
154
+ *
155
+ * Options are optional and default to `{ method: "POST" }`.
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * // Submit a plain object - fully type-safe (defaults to POST)
160
+ * await submitter.submitJson({
161
+ * email: "user@example.com",
162
+ * password: "secret123",
163
+ * rememberMe: true,
164
+ * });
165
+ *
166
+ * // Or specify a different method
167
+ * await submitter.submitJson(data, { method: "PUT" });
168
+ * ```
169
+ */
170
+ type SubmitJsonFunc<TModule extends RouteWithActionModule> = (data: z.infer<TModule["formSchema"]>, options?: SubmitJsonOptions) => Promise<void>;
171
+ /**
172
+ * Form component type with pre-bound action URL.
173
+ *
174
+ * Renders a form element that automatically submits to the correct route.
175
+ * Method defaults to "POST" if not specified.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * // Defaults to POST
180
+ * <submitter.Form>
181
+ * <input name="title" />
182
+ * <button type="submit">Submit</button>
183
+ * </submitter.Form>
184
+ *
185
+ * // Or specify a different method
186
+ * <submitter.Form method="PUT">
187
+ * ...
188
+ * </submitter.Form>
189
+ * ```
190
+ */
191
+ type SubmitForm = (props: Omit<FetcherFormProps & React.RefAttributes<HTMLFormElement>, "action" | "method"> & {
192
+ method?: Exclude<SubmitOptions["method"], "GET">;
193
+ }) => React.ReactElement;
194
+ /**
195
+ * Creates a type-safe fetcher for submitting forms to dynamic routes.
196
+ *
197
+ * This hook provides full TypeScript inference for:
198
+ * - Route parameters (from the route path)
199
+ * - Form data schema (from the route's formSchema export)
200
+ * - Action response type (from the route's action export)
201
+ *
202
+ * @template TInfo - The route module type (use `typeof import("./route-file")`)
203
+ *
204
+ * @param path - The route path (must match the route's `route` export)
205
+ * @param args - Route parameters (if the route has dynamic segments like `:id`)
206
+ *
207
+ * @returns An extended fetcher object with:
208
+ * - `submit` - Submit with FormData/SubmitTarget (includes schema type)
209
+ * - `submitJson` - Submit a plain object as JSON (schema type only)
210
+ * - `Form` - Pre-bound form component
211
+ * - `data` - Response data from the action (typed)
212
+ * - `state` - Fetcher state ("idle" | "loading" | "submitting")
213
+ * - All other useFetcher properties
214
+ *
215
+ * @example
216
+ * ### Basic usage with route parameters
217
+ *
218
+ * ```typescript
219
+ * // In your route file (app/routes/users.$userId.settings.tsx):
220
+ * export const route: RoutePath<"/users/:userId/settings"> = "/users/:userId/settings";
221
+ * export const formSchema = z.object({
222
+ * displayName: z.string().min(2),
223
+ * email: z.string().email(),
224
+ * notifications: z.boolean().default(true),
225
+ * });
226
+ * export const action = formAction({ schema: formSchema, handler: ... });
227
+ *
228
+ * // In your component:
229
+ * const submitter = useDynamicSubmitter<typeof import("./users.$userId.settings")>(
230
+ * "/users/:userId/settings",
231
+ * { userId: "123" }
232
+ * );
233
+ *
234
+ * // Submit using submitJson (type-safe, no FormData needed, defaults to POST)
235
+ * await submitter.submitJson({
236
+ * displayName: "John Doe",
237
+ * email: "john@example.com",
238
+ * notifications: true,
239
+ * });
240
+ *
241
+ * // Check the response
242
+ * if (submitter.data?.success) {
243
+ * console.log("Settings updated!");
244
+ * }
245
+ * ```
246
+ */
247
+ declare const useDynamicSubmitter: <TInfo extends RouteWithActionModule>(path: TInfo["route"], ...args: TInfo["route"] extends "undefined" ? HrefArgs<"/"> : HrefArgs<TInfo["route"]>) => Omit<ReturnType<typeof useFetcher<TInfo["action"]>>, "load" | "submit" | "Form"> & {
248
+ /** Submit with FormData or SubmitTarget (schema type & SubmitTarget) */
249
+ submit: SubmitFunc<TInfo>;
250
+ /** Submit a plain object as JSON (schema type only, defaults to POST) */
251
+ submitJson: SubmitJsonFunc<TInfo>;
252
+ /** Pre-bound Form component with action URL already set (defaults to POST) */
253
+ Form: SubmitForm;
254
+ };
255
+
256
+ export { useDynamicSubmitter };
@@ -0,0 +1,3 @@
1
+ export { useDynamicSubmitter } from './chunk-JJN6GBJL.js';
2
+ //# sourceMappingURL=useDynamicSubmitter.js.map
3
+ //# sourceMappingURL=useDynamicSubmitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"useDynamicSubmitter.js"}
@@ -0,0 +1,10 @@
1
+ import { useFetcher } from 'react-router';
2
+
3
+ /**
4
+ * A hook that tracks changes in a fetcher's state and calls a callback when it changes.
5
+ * @param fetcher The fetcher instance to track
6
+ * @param onChange Callback that receives the previous state and new state when the state changes
7
+ */
8
+ declare const useFetcherStateChanged: (fetcher: Pick<ReturnType<typeof useFetcher>, "state">, onChange: (lastState: typeof fetcher.state | undefined, newState: typeof fetcher.state) => void) => void;
9
+
10
+ export { useFetcherStateChanged };
@@ -0,0 +1,3 @@
1
+ export { useFetcherStateChanged } from './chunk-W5R7YYHR.js';
2
+ //# sourceMappingURL=useFetcherStateChanged.js.map
3
+ //# sourceMappingURL=useFetcherStateChanged.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"useFetcherStateChanged.js"}