@firtoz/router-toolkit 9.0.0 → 9.0.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
@@ -506,7 +506,7 @@ Type-safe form action wrapper that provides Zod validation and structured error
506
506
 
507
507
  - ✅ **Automatic form data validation** using Zod schemas
508
508
  - 🛡️ **Type-safe error handling** with structured error types
509
- - 🔄 **MaybeError integration** for consistent error patterns
509
+ - 🔄 **MaybeError integration** for consistent error patterns — import **`success`** / **`fail`** from [`@firtoz/maybe-error`](../maybe-error/README.md), not from router-toolkit (see [MaybeError Utility](#maybeerror-utility))
510
510
  - 🚀 **React Router compatibility** preserves redirects and responses
511
511
  - 📝 **Full TypeScript support** with inferred types from schemas
512
512
 
@@ -1184,6 +1184,8 @@ export default function CombinedTest() {
1184
1184
 
1185
1185
  `@firtoz/maybe-error` is a **dependency** of router-toolkit (it is installed with this package), but it is **not** re-exported from `@firtoz/router-toolkit`. Import `success`, `fail`, `MaybeError`, `exhaustiveGuard`, and related symbols **from `@firtoz/maybe-error`** so your imports match runtime and dependency boundaries stay clear.
1186
1186
 
1187
+ For the full API (`MaybeError`, `success`, `fail`, `exhaustiveGuard`, `AssumeSuccess`), see the [`@firtoz/maybe-error` README](../maybe-error/README.md).
1188
+
1187
1189
  ### Basic Usage
1188
1190
 
1189
1191
  ```tsx
@@ -1387,27 +1389,7 @@ export default function CreateUser() {
1387
1389
 
1388
1390
  ### MaybeError API Reference
1389
1391
 
1390
- ```tsx
1391
- // Type definitions
1392
- type MaybeError<T = undefined, TError = string> = DefiniteSuccess<T> | DefiniteError<TError>;
1393
-
1394
- type DefiniteSuccess<T> = {
1395
- success: true;
1396
- result: T; // Optional if T is undefined
1397
- };
1398
-
1399
- type DefiniteError<TError> = {
1400
- success: false;
1401
- error: TError;
1402
- };
1403
-
1404
- // Utility functions
1405
- const success = <T>(value: T): DefiniteSuccess<T> => ({ success: true, result: value });
1406
- const fail = <TError>(error: TError): DefiniteError<TError> => ({ success: false, error });
1407
-
1408
- // Type utility
1409
- type AssumeSuccess<T extends MaybeError<unknown>> = /* extracts the success type */;
1410
- ```
1392
+ See [`@firtoz/maybe-error` README](../maybe-error/README.md) for type definitions and utilities.
1411
1393
 
1412
1394
  **Benefits:**
1413
1395
  - **Type Safety**: TypeScript enforces error handling at compile time
@@ -179,5 +179,5 @@ function useDynamicSubmitterFetcher(submitter) {
179
179
  }
180
180
 
181
181
  export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher };
182
- //# sourceMappingURL=chunk-2WSA75KM.js.map
183
- //# sourceMappingURL=chunk-2WSA75KM.js.map
182
+ //# sourceMappingURL=chunk-XMGRKSHM.js.map
183
+ //# sourceMappingURL=chunk-XMGRKSHM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useDynamicSubmitter.tsx"],"names":["options"],"mappings":"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","file":"chunk-XMGRKSHM.js","sourcesContent":["/**\n * @fileoverview Type-safe dynamic form submission hook for React Router 7\n *\n * This module provides a hook that creates a type-safe fetcher for submitting forms\n * to dynamic routes with full TypeScript inference for the form schema and route params.\n *\n * **Awaiting results:** `submit` and `submitJson` return a `Promise` that resolves with the\n * action payload after the submission completes (fetcher leaves `submitting` / `loading` for\n * `idle`). The hook does **not** expose `data` or `state`—use the promise result (and local\n * React state) for outcomes and loading UX. For declarative UI tied to the same fetcher, use\n * {@link useDynamicSubmitterFetcher} (or {@link dynamicSubmitterFetcherKey} with `useFetcher`).\n * Use **one awaited submit at\n * a time** per hook instance; React Router’s\n * single fetcher replaces an in-flight request when you submit again. If you call `submit` or\n * `submitJson` again before the previous promise settles, the previous promise is **rejected**\n * with {@link SubmitterSupersededError}. That applies **per React Router fetcher key** (same\n * resolved URL and {@link UseDynamicSubmitterOptions.keySuffix}): two hook instances that share\n * the same key also supersede one another’s in-flight promise. Use distinct `keySuffix` values\n * when you need independent overlapping submissions to the same route. For many overlapping\n * operations, use `ConcurrentSubmitterProvider` / `useConcurrentSubmitter` instead.\n *\n * **Unmount:** If the component unmounts while a returned promise is still pending, that\n * promise is **rejected** with {@link SubmitterUnmountedError}.\n *\n * **Local `useState` vs {@link useDynamicSubmitterFetcher}:** For programmatic\n * `await submitter.submitJson`, a local pending flag and `try` / `finally` is often enough for\n * disabled buttons and matches the promise-first API. Use {@link useDynamicSubmitterFetcher} when\n * you want declarative `fetcher.state` / `fetcher.data` in JSX (especially with `submitter.Form`\n * or inline errors). The package README documents trade-offs in more detail.\n *\n * @example\n * ### Route Setup (`app/routes/admin.posts.$id.tsx`)\n *\n * First, set up your route with the required exports:\n *\n * ```typescript\n * import { z } from \"zod\";\n * import { formAction, type RoutePath } from \"@firtoz/router-toolkit\";\n * import { success, fail } from \"@firtoz/maybe-error\";\n *\n * // Export the route path for type inference\n * export const route: RoutePath<\"/admin/posts/:id\"> = \"/admin/posts/:id\";\n *\n * // Define the form schema\n * export const formSchema = z.object({\n * title: z.string().min(1, \"Title is required\"),\n * content: z.string().min(10, \"Content must be at least 10 characters\"),\n * published: z.boolean().optional().default(false),\n * });\n *\n * // Create the action using formAction\n * export const action = formAction({\n * schema: formSchema,\n * handler: async ({ request, params }, formData) => {\n * const postId = params.id;\n * const updated = await db.posts.update({\n * where: { id: postId },\n * data: formData,\n * });\n * return success(updated);\n * },\n * });\n * ```\n *\n * @example\n * ### Using the hook in a component\n *\n * ```tsx\n * import { useDynamicSubmitter } from \"@firtoz/router-toolkit\";\n *\n * function EditPostForm({ postId }: { postId: string }) {\n * // Type-safe submitter with full inference\n * const submitter = useDynamicSubmitter<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\n *\n * const [pending, setPending] = useState(false);\n *\n * // Option 1: Submit as JSON (recommended for programmatic submissions)\n * // Defaults to POST if no options provided\n * const handleSubmitJson = async () => {\n * setPending(true);\n * try {\n * const data = await submitter.submitJson({\n * title: \"My Post\",\n * content: \"Post content here\",\n * published: true,\n * });\n * if (data.success) {\n * console.log(\"Saved\");\n * }\n * } finally {\n * setPending(false);\n * }\n * };\n *\n * // Option 2: Submit with FormData or SubmitTarget\n * const handleSubmit = async (formData: FormData) => {\n * await submitter.submit(formData, { method: \"POST\" });\n * };\n *\n * // Option 3: Use the Form component (defaults to POST); pair with useDynamicSubmitterFetcher(submitter) if you need reactive state\n * return (\n * <submitter.Form>\n * <input name=\"title\" />\n * <textarea name=\"content\" />\n * <button type=\"submit\" disabled={pending}>Save</button>\n * </submitter.Form>\n * );\n * }\n * ```\n */\n\n// biome-ignore lint/style/useImportType: We need to import React here.\nimport React, { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport {\n\ttype FetcherFormProps,\n\ttype HTMLFormMethod,\n\thref,\n\ttype SubmitOptions,\n\ttype SubmitTarget,\n\tuseFetcher,\n} from \"react-router\";\nimport type { z } from \"zod\";\nimport type { HrefArgs } from \"./types/HrefArgs\";\nimport type { RouteWithActionModule } from \"./types/RouteWithActionModule\";\n\n/**\n * Thrown when a new `submit` or `submitJson` runs before a prior returned promise has settled.\n * The new submission proceeds; catch this error if overlapping calls are expected.\n */\nexport class SubmitterSupersededError extends Error {\n\toverride readonly name = \"SubmitterSupersededError\";\n\tconstructor(\n\t\tmessage = \"This submission was superseded by a newer submit before it completed.\",\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n/**\n * Thrown when the component that owns the submitter unmounts before a `submit` /\n * `submitJson` promise has settled.\n */\nexport class SubmitterUnmountedError extends Error {\n\toverride readonly name = \"SubmitterUnmountedError\";\n\tconstructor(\n\t\tmessage = \"The submitter was unmounted before this submission completed.\",\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n/**\n * Action payload type on the fetcher (same shape React Router puts on `fetcher.data` after the action runs).\n * Includes `undefined` while idle or in flight—use {@link SubmitterSettledData} for the value after\n * `await submitter.submit` / `await submitter.submitJson`.\n */\nexport type DynamicSubmitterData<TInfo extends RouteWithActionModule> =\n\tReturnType<typeof useFetcher<TInfo[\"action\"]>>[\"data\"];\n\n/**\n * Payload type after a successful `await submitter.submit` / `await submitter.submitJson`.\n * Omits `undefined` from {@link DynamicSubmitterData}: the promise only resolves when `fetcher.data`\n * is defined (otherwise it rejects). Inner success values may still be void / optional `result` for\n * `MaybeError<undefined>` from `formAction` + `success()`.\n */\nexport type SubmitterSettledData<TInfo extends RouteWithActionModule> =\n\tNonNullable<DynamicSubmitterData<TInfo>>;\n\n/**\n * Options for {@link useDynamicSubmitter}.\n */\nexport type UseDynamicSubmitterOptions = {\n\t/**\n\t * Appended to the default fetcher key so multiple submitters can target the same resolved URL\n\t * without sharing React Router fetcher state. Omit to use the default key for that URL.\n\t */\n\tkeySuffix?: string;\n};\n\n/**\n * React Router `useFetcher` key used by {@link useDynamicSubmitter} for a resolved href.\n * Pass the same string as {@link UseDynamicSubmitterResult.fetcherKey} (or call with the same\n * `resolvedHref` and `keySuffix` as the submitter) so a parallel `useFetcher({ key })` observes\n * the same submission lifecycle.\n *\n * When `keySuffix` is set, it is encoded and joined with a fixed delimiter so arbitrary strings\n * are safe in the key.\n */\nexport function dynamicSubmitterFetcherKey(\n\tresolvedHref: string,\n\tkeySuffix?: string,\n): string {\n\tconst base = `submitter-${resolvedHref}`;\n\tif (keySuffix === undefined || keySuffix === \"\") {\n\t\treturn base;\n\t}\n\treturn `${base}::${encodeURIComponent(keySuffix)}`;\n}\n\nfunction isSubmitterOptions(x: unknown): x is UseDynamicSubmitterOptions {\n\tif (x === null || typeof x !== \"object\") return false;\n\tconst keys = Object.keys(x as object);\n\tif (keys.length === 0) return false;\n\treturn keys.every((k) => k === \"keySuffix\");\n}\n\nfunction parseUseDynamicSubmitterRestArgs(args: readonly unknown[]): {\n\threfArgs: unknown[];\n\toptions: UseDynamicSubmitterOptions;\n} {\n\tif (args.length === 0) {\n\t\treturn { hrefArgs: [], options: {} };\n\t}\n\tconst last = args[args.length - 1];\n\tif (args.length >= 2 && isSubmitterOptions(last)) {\n\t\treturn { hrefArgs: [...args.slice(0, -1)], options: last };\n\t}\n\tif (args.length === 1 && isSubmitterOptions(args[0])) {\n\t\treturn { hrefArgs: [], options: args[0] };\n\t}\n\treturn { hrefArgs: [...args], options: {} };\n}\n\ntype UseDynamicSubmitterRest<R extends RouteWithActionModule[\"route\"]> =\n\tHrefArgs<R> extends readonly []\n\t\t? [options?: UseDynamicSubmitterOptions]\n\t\t: [...hrefArgs: HrefArgs<R>, options?: UseDynamicSubmitterOptions];\n\ntype PendingAwait = {\n\tgen: number;\n\townerId: number;\n\treject: (reason: unknown) => void;\n\t/** Called when the shared fetcher reaches `idle` for this submission generation. */\n\tfinishIdle: (data: unknown, error: unknown | undefined) => void;\n};\n\ntype SubmitterKeyBucket = {\n\tsubmitGen: number;\n\tpending: PendingAwait | null;\n};\n\nconst submitterKeyBuckets = new Map<string, SubmitterKeyBucket>();\n\nfunction getSubmitterKeyBucket(key: string): SubmitterKeyBucket {\n\tlet b = submitterKeyBuckets.get(key);\n\tif (!b) {\n\t\tb = { submitGen: 0, pending: null };\n\t\tsubmitterKeyBuckets.set(key, b);\n\t}\n\treturn b;\n}\n\nlet nextSubmitterOwnerId = 1;\nfunction allocateSubmitterOwnerId(): number {\n\treturn nextSubmitterOwnerId++;\n}\n\n/**\n * Function type for submitting form data with a SubmitTarget.\n *\n * Accepts the form schema data combined with SubmitTarget (FormData, HTMLFormElement, etc.)\n * Use this when you have a FormData object or form element reference.\n *\n * @example\n * ```typescript\n * // With FormData\n * submitter.submit(formData, { method: \"POST\" });\n *\n * // With form element reference\n * submitter.submit(formRef.current, { method: \"POST\" });\n * ```\n */\ntype SubmitFunc<TModule extends RouteWithActionModule> = (\n\ttarget: z.infer<TModule[\"formSchema\"]> & SubmitTarget,\n\toptions: Omit<SubmitOptions, \"action\" | \"method\" | \"encType\"> & {\n\t\tmethod: Exclude<SubmitOptions[\"method\"], \"GET\">;\n\t},\n) => Promise<SubmitterSettledData<TModule>>;\n\n/**\n * Options for submitJson function.\n * Method defaults to \"POST\" if not specified.\n */\ntype SubmitJsonOptions = Omit<\n\tSubmitOptions,\n\t\"action\" | \"method\" | \"encType\"\n> & {\n\tmethod?: Exclude<SubmitOptions[\"method\"], \"GET\">;\n};\n\n/**\n * Function type for submitting form data as JSON.\n *\n * Accepts only the inferred form schema type (plain object).\n * Automatically serializes the data as JSON. This is the recommended\n * approach for programmatic form submissions.\n *\n * Options are optional and default to `{ method: \"POST\" }`.\n *\n * @example\n * ```typescript\n * // Submit a plain object - fully type-safe (defaults to POST)\n * await submitter.submitJson({\n * email: \"user@example.com\",\n * password: \"secret123\",\n * rememberMe: true,\n * });\n *\n * // Or specify a different method\n * await submitter.submitJson(data, { method: \"PUT\" });\n * ```\n */\ntype SubmitJsonFunc<TModule extends RouteWithActionModule> = (\n\tdata: z.infer<TModule[\"formSchema\"]>,\n\toptions?: SubmitJsonOptions,\n) => Promise<SubmitterSettledData<TModule>>;\n\n/**\n * Form component type with pre-bound action URL.\n *\n * Renders a form element that automatically submits to the correct route.\n * Method defaults to \"POST\" if not specified.\n *\n * @example\n * ```typescript\n * // Defaults to POST\n * <submitter.Form>\n * <input name=\"title\" />\n * <button type=\"submit\">Submit</button>\n * </submitter.Form>\n *\n * // Or specify a different method\n * <submitter.Form method=\"PUT\">\n * ...\n * </submitter.Form>\n * ```\n */\ntype SubmitForm = (\n\tprops: Omit<\n\t\tFetcherFormProps & React.RefAttributes<HTMLFormElement>,\n\t\t\"action\" | \"method\"\n\t> & {\n\t\tmethod?: Exclude<SubmitOptions[\"method\"], \"GET\">;\n\t},\n) => React.ReactElement;\n\n/**\n * Stable object returned by {@link useDynamicSubmitter}: `submit`, `submitJson`, `Form`, and\n * `fetcherKey`. The reference is memoized and does not change when the internal fetcher’s\n * `state` / `data` update.\n */\nexport type UseDynamicSubmitterResult<TInfo extends RouteWithActionModule> = {\n\tsubmit: SubmitFunc<TInfo>;\n\tsubmitJson: SubmitJsonFunc<TInfo>;\n\tForm: SubmitForm;\n\t/** Pass to {@link useDynamicSubmitterFetcher} or `useFetcher({ key })` for reactive `state` / `data`. */\n\tfetcherKey: string;\n};\n\n/**\n * Creates a type-safe fetcher for submitting forms to dynamic routes.\n *\n * This hook provides full TypeScript inference for:\n * - Route parameters (from the route path)\n * - Form data schema (from the route's formSchema export)\n * - Action response type (from the route's action export)\n *\n * @template TInfo - The route module type (use `typeof import(\"./route-file\")`)\n *\n * @param path - The route path (must match the route's `route` export)\n * @param rest - Route parameters (if any), then optional {@link UseDynamicSubmitterOptions}. For\n * static routes, you may pass only options as the second argument (e.g. `{ keySuffix: \"a\" }`).\n * Options are recognized only when the object contains exclusively the `keySuffix` key (do not use\n * a route param object whose only field is named `keySuffix` unless it is meant as options).\n *\n * @returns Stable `{ submit, submitJson, Form, fetcherKey }`. Await the promises for action results;\n * use {@link useDynamicSubmitterFetcher} or local state for reactive loading/data.\n *\n * @example\n * ### Basic usage with route parameters\n *\n * ```typescript\n * // In your route file (app/routes/users.$userId.settings.tsx):\n * export const route: RoutePath<\"/users/:userId/settings\"> = \"/users/:userId/settings\";\n * export const formSchema = z.object({\n * displayName: z.string().min(2),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * });\n * export const action = formAction({ schema: formSchema, handler: ... });\n *\n * // In your component:\n * const submitter = useDynamicSubmitter<typeof import(\"./users.$userId.settings\")>(\n * \"/users/:userId/settings\",\n * { userId: \"123\" }\n * );\n *\n * const data = await submitter.submitJson({\n * displayName: \"John Doe\",\n * email: \"john@example.com\",\n * notifications: true,\n * });\n *\n * if (data.success) {\n * console.log(\"Settings updated!\");\n * }\n * ```\n */\nexport function useDynamicSubmitter<TInfo extends RouteWithActionModule>(\n\tpath: TInfo[\"route\"],\n\t...rest: UseDynamicSubmitterRest<TInfo[\"route\"]>\n): UseDynamicSubmitterResult<TInfo> {\n\tconst { hrefArgs, options } = parseUseDynamicSubmitterRestArgs(rest);\n\tconst keySuffix = options.keySuffix;\n\n\tconst url = useMemo(() => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Intentional\n\t\treturn href(path, ...(hrefArgs as any));\n\t}, [path, keySuffix, ...(hrefArgs as unknown[])]);\n\n\tconst fetcherKey = useMemo(\n\t\t() => dynamicSubmitterFetcherKey(url, keySuffix),\n\t\t[url, keySuffix],\n\t);\n\n\tconst fetcher = useFetcher<TInfo[\"action\"]>({\n\t\tkey: fetcherKey,\n\t});\n\n\tconst fetcherRef = useRef(fetcher);\n\tfetcherRef.current = fetcher;\n\n\tconst ownerIdRef = useRef(allocateSubmitterOwnerId());\n\tconst prevStateRef = useRef(fetcher.state);\n\n\tconst beginSubmit = useCallback(\n\t\t(runSubmit: () => void) => {\n\t\t\treturn new Promise<SubmitterSettledData<TInfo>>((resolve, reject) => {\n\t\t\t\tconst bucket = getSubmitterKeyBucket(fetcherKey);\n\t\t\t\tconst prevPending = bucket.pending;\n\t\t\t\tif (prevPending) {\n\t\t\t\t\tprevPending.reject(new SubmitterSupersededError());\n\t\t\t\t}\n\t\t\t\tbucket.submitGen += 1;\n\t\t\t\tconst gen = bucket.submitGen;\n\t\t\t\tbucket.pending = {\n\t\t\t\t\tgen,\n\t\t\t\t\townerId: ownerIdRef.current,\n\t\t\t\t\treject,\n\t\t\t\t\tfinishIdle: (data, error) => {\n\t\t\t\t\t\tif (data !== undefined) {\n\t\t\t\t\t\t\tresolve(data as SubmitterSettledData<TInfo>);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treject(error ?? new Error(\"Submission failed\"));\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t\trunSubmit();\n\t\t\t});\n\t\t},\n\t\t[fetcherKey],\n\t);\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tconst bucket = getSubmitterKeyBucket(fetcherKey);\n\t\t\tconst pending = bucket.pending;\n\t\t\tif (pending && pending.ownerId === ownerIdRef.current) {\n\t\t\t\tbucket.pending = null;\n\t\t\t\tpending.reject(new SubmitterUnmountedError());\n\t\t\t}\n\t\t};\n\t}, [fetcherKey]);\n\n\tconst fetcherError = (fetcher as { error?: unknown }).error;\n\n\tuseEffect(() => {\n\t\tconst prev = prevStateRef.current;\n\t\tprevStateRef.current = fetcher.state;\n\t\tconst wasWorking = prev === \"submitting\" || prev === \"loading\";\n\t\tif (!wasWorking || fetcher.state !== \"idle\") {\n\t\t\treturn;\n\t\t}\n\t\tconst bucket = getSubmitterKeyBucket(fetcherKey);\n\t\tconst p = bucket.pending;\n\t\tif (!p || p.gen !== bucket.submitGen) {\n\t\t\treturn;\n\t\t}\n\t\tbucket.pending = null;\n\t\tp.finishIdle(fetcher.data, fetcherError);\n\t}, [fetcherKey, fetcher.state, fetcher.data, fetcherError]);\n\n\tconst submit: SubmitFunc<TInfo> = useCallback(\n\t\t(target, options) => {\n\t\t\treturn beginSubmit(() => {\n\t\t\t\tconst f = fetcherRef.current;\n\t\t\t\tvoid f.submit(target, {\n\t\t\t\t\t...options,\n\t\t\t\t\tmethod: (options?.method ?? \"POST\") as HTMLFormMethod,\n\t\t\t\t\taction: url,\n\t\t\t\t\tencType: \"multipart/form-data\",\n\t\t\t\t} as Parameters<typeof f.submit>[1]);\n\t\t\t});\n\t\t},\n\t\t[beginSubmit, url],\n\t);\n\n\tconst submitJson: SubmitJsonFunc<TInfo> = useCallback(\n\t\t(data, options = {}) => {\n\t\t\treturn beginSubmit(() => {\n\t\t\t\tconst f = fetcherRef.current;\n\t\t\t\tvoid f.submit(\n\t\t\t\t\tdata as SubmitTarget,\n\t\t\t\t\t{\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\tmethod: (options.method ?? \"POST\") as HTMLFormMethod,\n\t\t\t\t\t\taction: url,\n\t\t\t\t\t\tencType: \"application/json\",\n\t\t\t\t\t} as Parameters<typeof f.submit>[1],\n\t\t\t\t);\n\t\t\t});\n\t\t},\n\t\t[beginSubmit, url],\n\t);\n\n\tconst fetcherFormRef = useRef(fetcher.Form);\n\tfetcherFormRef.current = fetcher.Form;\n\n\tconst Form: SubmitForm = useCallback(\n\t\t({ method = \"POST\", ...props }) => {\n\t\t\tconst OriginalForm = fetcherFormRef.current;\n\t\t\treturn <OriginalForm action={url} method={method} {...props} />;\n\t\t},\n\t\t[url],\n\t);\n\n\treturn useMemo(\n\t\t() => ({\n\t\t\tsubmit,\n\t\t\tsubmitJson,\n\t\t\tForm,\n\t\t\tfetcherKey,\n\t\t}),\n\t\t[submit, submitJson, Form, fetcherKey],\n\t);\n}\n\n/**\n * React Router `useFetcher` bound to the same key as {@link useDynamicSubmitter}, so `state` /\n * `data` reflect the same submissions as `submitter.submit` / `submitter.Form`.\n *\n * Call at component top level next to `useDynamicSubmitter`.\n */\nexport function useDynamicSubmitterFetcher<TInfo extends RouteWithActionModule>(\n\tsubmitter: UseDynamicSubmitterResult<TInfo>,\n) {\n\treturn useFetcher<TInfo[\"action\"]>({ key: submitter.fetcherKey });\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { RouteWithLoaderModule } from './types/RouteWithLoaderModule.js';
9
9
  export { useCachedFetch } from './useCachedFetch.js';
10
10
  export { UseConcurrentSubmitterReturn, useConcurrentSubmitter } from './useConcurrentSubmitter.js';
11
11
  export { useDynamicFetcher } from './useDynamicFetcher.js';
12
- export { DynamicSubmitterData, SubmitterSupersededError, SubmitterUnmountedError, UseDynamicSubmitterOptions, UseDynamicSubmitterResult, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './useDynamicSubmitter.js';
12
+ export { DynamicSubmitterData, SubmitterSettledData, SubmitterSupersededError, SubmitterUnmountedError, UseDynamicSubmitterOptions, UseDynamicSubmitterResult, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './useDynamicSubmitter.js';
13
13
  export { useFetcherStateChanged } from './useFetcherStateChanged.js';
14
14
  import 'react/jsx-runtime';
15
15
  import 'react';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import './chunk-2W5QFQTE.js';
3
3
  export { useCachedFetch } from './chunk-OQGTU34H.js';
4
4
  export { useConcurrentSubmitter } from './chunk-QZDEBX3D.js';
5
5
  export { useDynamicFetcher } from './chunk-F4324Q33.js';
6
- export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './chunk-2WSA75KM.js';
6
+ export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './chunk-XMGRKSHM.js';
7
7
  export { useFetcherStateChanged } from './chunk-W5R7YYHR.js';
8
8
  export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider } from './chunk-ZC6U3MIB.js';
9
9
  export { formAction } from './chunk-UF5QHE5K.js';
@@ -246,9 +246,18 @@ declare class SubmitterUnmountedError extends Error {
246
246
  constructor(message?: string);
247
247
  }
248
248
  /**
249
- * Action payload type resolved by `submit` / `submitJson` (same shape React Router puts on `fetcher.data` after the action runs).
249
+ * Action payload type on the fetcher (same shape React Router puts on `fetcher.data` after the action runs).
250
+ * Includes `undefined` while idle or in flight—use {@link SubmitterSettledData} for the value after
251
+ * `await submitter.submit` / `await submitter.submitJson`.
250
252
  */
251
253
  type DynamicSubmitterData<TInfo extends RouteWithActionModule> = ReturnType<typeof useFetcher<TInfo["action"]>>["data"];
254
+ /**
255
+ * Payload type after a successful `await submitter.submit` / `await submitter.submitJson`.
256
+ * Omits `undefined` from {@link DynamicSubmitterData}: the promise only resolves when `fetcher.data`
257
+ * is defined (otherwise it rejects). Inner success values may still be void / optional `result` for
258
+ * `MaybeError<undefined>` from `formAction` + `success()`.
259
+ */
260
+ type SubmitterSettledData<TInfo extends RouteWithActionModule> = NonNullable<DynamicSubmitterData<TInfo>>;
252
261
  /**
253
262
  * Options for {@link useDynamicSubmitter}.
254
263
  */
@@ -287,7 +296,7 @@ type UseDynamicSubmitterRest<R extends RouteWithActionModule["route"]> = HrefArg
287
296
  */
288
297
  type SubmitFunc<TModule extends RouteWithActionModule> = (target: z.infer<TModule["formSchema"]> & SubmitTarget, options: Omit<SubmitOptions, "action" | "method" | "encType"> & {
289
298
  method: Exclude<SubmitOptions["method"], "GET">;
290
- }) => Promise<DynamicSubmitterData<TModule>>;
299
+ }) => Promise<SubmitterSettledData<TModule>>;
291
300
  /**
292
301
  * Options for submitJson function.
293
302
  * Method defaults to "POST" if not specified.
@@ -317,7 +326,7 @@ type SubmitJsonOptions = Omit<SubmitOptions, "action" | "method" | "encType"> &
317
326
  * await submitter.submitJson(data, { method: "PUT" });
318
327
  * ```
319
328
  */
320
- type SubmitJsonFunc<TModule extends RouteWithActionModule> = (data: z.infer<TModule["formSchema"]>, options?: SubmitJsonOptions) => Promise<DynamicSubmitterData<TModule>>;
329
+ type SubmitJsonFunc<TModule extends RouteWithActionModule> = (data: z.infer<TModule["formSchema"]>, options?: SubmitJsonOptions) => Promise<SubmitterSettledData<TModule>>;
321
330
  /**
322
331
  * Form component type with pre-bound action URL.
323
332
  *
@@ -397,7 +406,7 @@ type UseDynamicSubmitterResult<TInfo extends RouteWithActionModule> = {
397
406
  * notifications: true,
398
407
  * });
399
408
  *
400
- * if (data?.success) {
409
+ * if (data.success) {
401
410
  * console.log("Settings updated!");
402
411
  * }
403
412
  * ```
@@ -411,4 +420,4 @@ declare function useDynamicSubmitter<TInfo extends RouteWithActionModule>(path:
411
420
  */
412
421
  declare function useDynamicSubmitterFetcher<TInfo extends RouteWithActionModule>(submitter: UseDynamicSubmitterResult<TInfo>): react_router.FetcherWithComponents<SerializeFrom<TInfo["action"]>>;
413
422
 
414
- export { type DynamicSubmitterData, SubmitterSupersededError, SubmitterUnmountedError, type UseDynamicSubmitterOptions, type UseDynamicSubmitterResult, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher };
423
+ export { type DynamicSubmitterData, type SubmitterSettledData, SubmitterSupersededError, SubmitterUnmountedError, type UseDynamicSubmitterOptions, type UseDynamicSubmitterResult, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher };
@@ -1,3 +1,3 @@
1
- export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './chunk-2WSA75KM.js';
1
+ export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './chunk-XMGRKSHM.js';
2
2
  //# sourceMappingURL=useDynamicSubmitter.js.map
3
3
  //# sourceMappingURL=useDynamicSubmitter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/router-toolkit",
3
- "version": "9.0.0",
3
+ "version": "9.0.2",
4
4
  "description": "Type-safe React Router 7 framework mode helpers with enhanced fetching, form submission, and state management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,7 +66,7 @@
66
66
  "peerDependencies": {
67
67
  "react": "^19.2.5",
68
68
  "react-router": "^7.14.2",
69
- "zod": "^4.3.6"
69
+ "zod": "^4.4.2"
70
70
  },
71
71
  "engines": {
72
72
  "node": ">=18.0.0"
@@ -75,7 +75,7 @@
75
75
  "access": "public"
76
76
  },
77
77
  "dependencies": {
78
- "@firtoz/maybe-error": "^1.6.1",
78
+ "@firtoz/maybe-error": "^1.6.2",
79
79
  "zod-form-data": "^3.0.1"
80
80
  },
81
81
  "devDependencies": {
@@ -83,6 +83,6 @@
83
83
  "@types/jsdom": "^28.0.1",
84
84
  "@types/react": "^19.2.14",
85
85
  "bun-types": "^1.3.13",
86
- "jsdom": "^29.0.2"
86
+ "jsdom": "^29.1.1"
87
87
  }
88
88
  }
@@ -87,7 +87,7 @@
87
87
  * content: "Post content here",
88
88
  * published: true,
89
89
  * });
90
- * if (data?.success) {
90
+ * if (data.success) {
91
91
  * console.log("Saved");
92
92
  * }
93
93
  * } finally {
@@ -153,11 +153,22 @@ export class SubmitterUnmountedError extends Error {
153
153
  }
154
154
 
155
155
  /**
156
- * Action payload type resolved by `submit` / `submitJson` (same shape React Router puts on `fetcher.data` after the action runs).
156
+ * Action payload type on the fetcher (same shape React Router puts on `fetcher.data` after the action runs).
157
+ * Includes `undefined` while idle or in flight—use {@link SubmitterSettledData} for the value after
158
+ * `await submitter.submit` / `await submitter.submitJson`.
157
159
  */
158
160
  export type DynamicSubmitterData<TInfo extends RouteWithActionModule> =
159
161
  ReturnType<typeof useFetcher<TInfo["action"]>>["data"];
160
162
 
163
+ /**
164
+ * Payload type after a successful `await submitter.submit` / `await submitter.submitJson`.
165
+ * Omits `undefined` from {@link DynamicSubmitterData}: the promise only resolves when `fetcher.data`
166
+ * is defined (otherwise it rejects). Inner success values may still be void / optional `result` for
167
+ * `MaybeError<undefined>` from `formAction` + `success()`.
168
+ */
169
+ export type SubmitterSettledData<TInfo extends RouteWithActionModule> =
170
+ NonNullable<DynamicSubmitterData<TInfo>>;
171
+
161
172
  /**
162
173
  * Options for {@link useDynamicSubmitter}.
163
174
  */
@@ -267,7 +278,7 @@ type SubmitFunc<TModule extends RouteWithActionModule> = (
267
278
  options: Omit<SubmitOptions, "action" | "method" | "encType"> & {
268
279
  method: Exclude<SubmitOptions["method"], "GET">;
269
280
  },
270
- ) => Promise<DynamicSubmitterData<TModule>>;
281
+ ) => Promise<SubmitterSettledData<TModule>>;
271
282
 
272
283
  /**
273
284
  * Options for submitJson function.
@@ -305,7 +316,7 @@ type SubmitJsonOptions = Omit<
305
316
  type SubmitJsonFunc<TModule extends RouteWithActionModule> = (
306
317
  data: z.infer<TModule["formSchema"]>,
307
318
  options?: SubmitJsonOptions,
308
- ) => Promise<DynamicSubmitterData<TModule>>;
319
+ ) => Promise<SubmitterSettledData<TModule>>;
309
320
 
310
321
  /**
311
322
  * Form component type with pre-bound action URL.
@@ -393,7 +404,7 @@ export type UseDynamicSubmitterResult<TInfo extends RouteWithActionModule> = {
393
404
  * notifications: true,
394
405
  * });
395
406
  *
396
- * if (data?.success) {
407
+ * if (data.success) {
397
408
  * console.log("Settings updated!");
398
409
  * }
399
410
  * ```
@@ -427,7 +438,7 @@ export function useDynamicSubmitter<TInfo extends RouteWithActionModule>(
427
438
 
428
439
  const beginSubmit = useCallback(
429
440
  (runSubmit: () => void) => {
430
- return new Promise<DynamicSubmitterData<TInfo>>((resolve, reject) => {
441
+ return new Promise<SubmitterSettledData<TInfo>>((resolve, reject) => {
431
442
  const bucket = getSubmitterKeyBucket(fetcherKey);
432
443
  const prevPending = bucket.pending;
433
444
  if (prevPending) {
@@ -441,7 +452,7 @@ export function useDynamicSubmitter<TInfo extends RouteWithActionModule>(
441
452
  reject,
442
453
  finishIdle: (data, error) => {
443
454
  if (data !== undefined) {
444
- resolve(data as DynamicSubmitterData<TInfo>);
455
+ resolve(data as SubmitterSettledData<TInfo>);
445
456
  } else {
446
457
  reject(error ?? new Error("Submission failed"));
447
458
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useDynamicSubmitter.tsx"],"names":["options"],"mappings":"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","file":"chunk-2WSA75KM.js","sourcesContent":["/**\n * @fileoverview Type-safe dynamic form submission hook for React Router 7\n *\n * This module provides a hook that creates a type-safe fetcher for submitting forms\n * to dynamic routes with full TypeScript inference for the form schema and route params.\n *\n * **Awaiting results:** `submit` and `submitJson` return a `Promise` that resolves with the\n * action payload after the submission completes (fetcher leaves `submitting` / `loading` for\n * `idle`). The hook does **not** expose `data` or `state`—use the promise result (and local\n * React state) for outcomes and loading UX. For declarative UI tied to the same fetcher, use\n * {@link useDynamicSubmitterFetcher} (or {@link dynamicSubmitterFetcherKey} with `useFetcher`).\n * Use **one awaited submit at\n * a time** per hook instance; React Router’s\n * single fetcher replaces an in-flight request when you submit again. If you call `submit` or\n * `submitJson` again before the previous promise settles, the previous promise is **rejected**\n * with {@link SubmitterSupersededError}. That applies **per React Router fetcher key** (same\n * resolved URL and {@link UseDynamicSubmitterOptions.keySuffix}): two hook instances that share\n * the same key also supersede one another’s in-flight promise. Use distinct `keySuffix` values\n * when you need independent overlapping submissions to the same route. For many overlapping\n * operations, use `ConcurrentSubmitterProvider` / `useConcurrentSubmitter` instead.\n *\n * **Unmount:** If the component unmounts while a returned promise is still pending, that\n * promise is **rejected** with {@link SubmitterUnmountedError}.\n *\n * **Local `useState` vs {@link useDynamicSubmitterFetcher}:** For programmatic\n * `await submitter.submitJson`, a local pending flag and `try` / `finally` is often enough for\n * disabled buttons and matches the promise-first API. Use {@link useDynamicSubmitterFetcher} when\n * you want declarative `fetcher.state` / `fetcher.data` in JSX (especially with `submitter.Form`\n * or inline errors). The package README documents trade-offs in more detail.\n *\n * @example\n * ### Route Setup (`app/routes/admin.posts.$id.tsx`)\n *\n * First, set up your route with the required exports:\n *\n * ```typescript\n * import { z } from \"zod\";\n * import { formAction, type RoutePath } from \"@firtoz/router-toolkit\";\n * import { success, fail } from \"@firtoz/maybe-error\";\n *\n * // Export the route path for type inference\n * export const route: RoutePath<\"/admin/posts/:id\"> = \"/admin/posts/:id\";\n *\n * // Define the form schema\n * export const formSchema = z.object({\n * title: z.string().min(1, \"Title is required\"),\n * content: z.string().min(10, \"Content must be at least 10 characters\"),\n * published: z.boolean().optional().default(false),\n * });\n *\n * // Create the action using formAction\n * export const action = formAction({\n * schema: formSchema,\n * handler: async ({ request, params }, formData) => {\n * const postId = params.id;\n * const updated = await db.posts.update({\n * where: { id: postId },\n * data: formData,\n * });\n * return success(updated);\n * },\n * });\n * ```\n *\n * @example\n * ### Using the hook in a component\n *\n * ```tsx\n * import { useDynamicSubmitter } from \"@firtoz/router-toolkit\";\n *\n * function EditPostForm({ postId }: { postId: string }) {\n * // Type-safe submitter with full inference\n * const submitter = useDynamicSubmitter<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\n *\n * const [pending, setPending] = useState(false);\n *\n * // Option 1: Submit as JSON (recommended for programmatic submissions)\n * // Defaults to POST if no options provided\n * const handleSubmitJson = async () => {\n * setPending(true);\n * try {\n * const data = await submitter.submitJson({\n * title: \"My Post\",\n * content: \"Post content here\",\n * published: true,\n * });\n * if (data?.success) {\n * console.log(\"Saved\");\n * }\n * } finally {\n * setPending(false);\n * }\n * };\n *\n * // Option 2: Submit with FormData or SubmitTarget\n * const handleSubmit = async (formData: FormData) => {\n * await submitter.submit(formData, { method: \"POST\" });\n * };\n *\n * // Option 3: Use the Form component (defaults to POST); pair with useDynamicSubmitterFetcher(submitter) if you need reactive state\n * return (\n * <submitter.Form>\n * <input name=\"title\" />\n * <textarea name=\"content\" />\n * <button type=\"submit\" disabled={pending}>Save</button>\n * </submitter.Form>\n * );\n * }\n * ```\n */\n\n// biome-ignore lint/style/useImportType: We need to import React here.\nimport React, { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport {\n\ttype FetcherFormProps,\n\ttype HTMLFormMethod,\n\thref,\n\ttype SubmitOptions,\n\ttype SubmitTarget,\n\tuseFetcher,\n} from \"react-router\";\nimport type { z } from \"zod\";\nimport type { HrefArgs } from \"./types/HrefArgs\";\nimport type { RouteWithActionModule } from \"./types/RouteWithActionModule\";\n\n/**\n * Thrown when a new `submit` or `submitJson` runs before a prior returned promise has settled.\n * The new submission proceeds; catch this error if overlapping calls are expected.\n */\nexport class SubmitterSupersededError extends Error {\n\toverride readonly name = \"SubmitterSupersededError\";\n\tconstructor(\n\t\tmessage = \"This submission was superseded by a newer submit before it completed.\",\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n/**\n * Thrown when the component that owns the submitter unmounts before a `submit` /\n * `submitJson` promise has settled.\n */\nexport class SubmitterUnmountedError extends Error {\n\toverride readonly name = \"SubmitterUnmountedError\";\n\tconstructor(\n\t\tmessage = \"The submitter was unmounted before this submission completed.\",\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n/**\n * Action payload type resolved by `submit` / `submitJson` (same shape React Router puts on `fetcher.data` after the action runs).\n */\nexport type DynamicSubmitterData<TInfo extends RouteWithActionModule> =\n\tReturnType<typeof useFetcher<TInfo[\"action\"]>>[\"data\"];\n\n/**\n * Options for {@link useDynamicSubmitter}.\n */\nexport type UseDynamicSubmitterOptions = {\n\t/**\n\t * Appended to the default fetcher key so multiple submitters can target the same resolved URL\n\t * without sharing React Router fetcher state. Omit to use the default key for that URL.\n\t */\n\tkeySuffix?: string;\n};\n\n/**\n * React Router `useFetcher` key used by {@link useDynamicSubmitter} for a resolved href.\n * Pass the same string as {@link UseDynamicSubmitterResult.fetcherKey} (or call with the same\n * `resolvedHref` and `keySuffix` as the submitter) so a parallel `useFetcher({ key })` observes\n * the same submission lifecycle.\n *\n * When `keySuffix` is set, it is encoded and joined with a fixed delimiter so arbitrary strings\n * are safe in the key.\n */\nexport function dynamicSubmitterFetcherKey(\n\tresolvedHref: string,\n\tkeySuffix?: string,\n): string {\n\tconst base = `submitter-${resolvedHref}`;\n\tif (keySuffix === undefined || keySuffix === \"\") {\n\t\treturn base;\n\t}\n\treturn `${base}::${encodeURIComponent(keySuffix)}`;\n}\n\nfunction isSubmitterOptions(x: unknown): x is UseDynamicSubmitterOptions {\n\tif (x === null || typeof x !== \"object\") return false;\n\tconst keys = Object.keys(x as object);\n\tif (keys.length === 0) return false;\n\treturn keys.every((k) => k === \"keySuffix\");\n}\n\nfunction parseUseDynamicSubmitterRestArgs(args: readonly unknown[]): {\n\threfArgs: unknown[];\n\toptions: UseDynamicSubmitterOptions;\n} {\n\tif (args.length === 0) {\n\t\treturn { hrefArgs: [], options: {} };\n\t}\n\tconst last = args[args.length - 1];\n\tif (args.length >= 2 && isSubmitterOptions(last)) {\n\t\treturn { hrefArgs: [...args.slice(0, -1)], options: last };\n\t}\n\tif (args.length === 1 && isSubmitterOptions(args[0])) {\n\t\treturn { hrefArgs: [], options: args[0] };\n\t}\n\treturn { hrefArgs: [...args], options: {} };\n}\n\ntype UseDynamicSubmitterRest<R extends RouteWithActionModule[\"route\"]> =\n\tHrefArgs<R> extends readonly []\n\t\t? [options?: UseDynamicSubmitterOptions]\n\t\t: [...hrefArgs: HrefArgs<R>, options?: UseDynamicSubmitterOptions];\n\ntype PendingAwait = {\n\tgen: number;\n\townerId: number;\n\treject: (reason: unknown) => void;\n\t/** Called when the shared fetcher reaches `idle` for this submission generation. */\n\tfinishIdle: (data: unknown, error: unknown | undefined) => void;\n};\n\ntype SubmitterKeyBucket = {\n\tsubmitGen: number;\n\tpending: PendingAwait | null;\n};\n\nconst submitterKeyBuckets = new Map<string, SubmitterKeyBucket>();\n\nfunction getSubmitterKeyBucket(key: string): SubmitterKeyBucket {\n\tlet b = submitterKeyBuckets.get(key);\n\tif (!b) {\n\t\tb = { submitGen: 0, pending: null };\n\t\tsubmitterKeyBuckets.set(key, b);\n\t}\n\treturn b;\n}\n\nlet nextSubmitterOwnerId = 1;\nfunction allocateSubmitterOwnerId(): number {\n\treturn nextSubmitterOwnerId++;\n}\n\n/**\n * Function type for submitting form data with a SubmitTarget.\n *\n * Accepts the form schema data combined with SubmitTarget (FormData, HTMLFormElement, etc.)\n * Use this when you have a FormData object or form element reference.\n *\n * @example\n * ```typescript\n * // With FormData\n * submitter.submit(formData, { method: \"POST\" });\n *\n * // With form element reference\n * submitter.submit(formRef.current, { method: \"POST\" });\n * ```\n */\ntype SubmitFunc<TModule extends RouteWithActionModule> = (\n\ttarget: z.infer<TModule[\"formSchema\"]> & SubmitTarget,\n\toptions: Omit<SubmitOptions, \"action\" | \"method\" | \"encType\"> & {\n\t\tmethod: Exclude<SubmitOptions[\"method\"], \"GET\">;\n\t},\n) => Promise<DynamicSubmitterData<TModule>>;\n\n/**\n * Options for submitJson function.\n * Method defaults to \"POST\" if not specified.\n */\ntype SubmitJsonOptions = Omit<\n\tSubmitOptions,\n\t\"action\" | \"method\" | \"encType\"\n> & {\n\tmethod?: Exclude<SubmitOptions[\"method\"], \"GET\">;\n};\n\n/**\n * Function type for submitting form data as JSON.\n *\n * Accepts only the inferred form schema type (plain object).\n * Automatically serializes the data as JSON. This is the recommended\n * approach for programmatic form submissions.\n *\n * Options are optional and default to `{ method: \"POST\" }`.\n *\n * @example\n * ```typescript\n * // Submit a plain object - fully type-safe (defaults to POST)\n * await submitter.submitJson({\n * email: \"user@example.com\",\n * password: \"secret123\",\n * rememberMe: true,\n * });\n *\n * // Or specify a different method\n * await submitter.submitJson(data, { method: \"PUT\" });\n * ```\n */\ntype SubmitJsonFunc<TModule extends RouteWithActionModule> = (\n\tdata: z.infer<TModule[\"formSchema\"]>,\n\toptions?: SubmitJsonOptions,\n) => Promise<DynamicSubmitterData<TModule>>;\n\n/**\n * Form component type with pre-bound action URL.\n *\n * Renders a form element that automatically submits to the correct route.\n * Method defaults to \"POST\" if not specified.\n *\n * @example\n * ```typescript\n * // Defaults to POST\n * <submitter.Form>\n * <input name=\"title\" />\n * <button type=\"submit\">Submit</button>\n * </submitter.Form>\n *\n * // Or specify a different method\n * <submitter.Form method=\"PUT\">\n * ...\n * </submitter.Form>\n * ```\n */\ntype SubmitForm = (\n\tprops: Omit<\n\t\tFetcherFormProps & React.RefAttributes<HTMLFormElement>,\n\t\t\"action\" | \"method\"\n\t> & {\n\t\tmethod?: Exclude<SubmitOptions[\"method\"], \"GET\">;\n\t},\n) => React.ReactElement;\n\n/**\n * Stable object returned by {@link useDynamicSubmitter}: `submit`, `submitJson`, `Form`, and\n * `fetcherKey`. The reference is memoized and does not change when the internal fetcher’s\n * `state` / `data` update.\n */\nexport type UseDynamicSubmitterResult<TInfo extends RouteWithActionModule> = {\n\tsubmit: SubmitFunc<TInfo>;\n\tsubmitJson: SubmitJsonFunc<TInfo>;\n\tForm: SubmitForm;\n\t/** Pass to {@link useDynamicSubmitterFetcher} or `useFetcher({ key })` for reactive `state` / `data`. */\n\tfetcherKey: string;\n};\n\n/**\n * Creates a type-safe fetcher for submitting forms to dynamic routes.\n *\n * This hook provides full TypeScript inference for:\n * - Route parameters (from the route path)\n * - Form data schema (from the route's formSchema export)\n * - Action response type (from the route's action export)\n *\n * @template TInfo - The route module type (use `typeof import(\"./route-file\")`)\n *\n * @param path - The route path (must match the route's `route` export)\n * @param rest - Route parameters (if any), then optional {@link UseDynamicSubmitterOptions}. For\n * static routes, you may pass only options as the second argument (e.g. `{ keySuffix: \"a\" }`).\n * Options are recognized only when the object contains exclusively the `keySuffix` key (do not use\n * a route param object whose only field is named `keySuffix` unless it is meant as options).\n *\n * @returns Stable `{ submit, submitJson, Form, fetcherKey }`. Await the promises for action results;\n * use {@link useDynamicSubmitterFetcher} or local state for reactive loading/data.\n *\n * @example\n * ### Basic usage with route parameters\n *\n * ```typescript\n * // In your route file (app/routes/users.$userId.settings.tsx):\n * export const route: RoutePath<\"/users/:userId/settings\"> = \"/users/:userId/settings\";\n * export const formSchema = z.object({\n * displayName: z.string().min(2),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * });\n * export const action = formAction({ schema: formSchema, handler: ... });\n *\n * // In your component:\n * const submitter = useDynamicSubmitter<typeof import(\"./users.$userId.settings\")>(\n * \"/users/:userId/settings\",\n * { userId: \"123\" }\n * );\n *\n * const data = await submitter.submitJson({\n * displayName: \"John Doe\",\n * email: \"john@example.com\",\n * notifications: true,\n * });\n *\n * if (data?.success) {\n * console.log(\"Settings updated!\");\n * }\n * ```\n */\nexport function useDynamicSubmitter<TInfo extends RouteWithActionModule>(\n\tpath: TInfo[\"route\"],\n\t...rest: UseDynamicSubmitterRest<TInfo[\"route\"]>\n): UseDynamicSubmitterResult<TInfo> {\n\tconst { hrefArgs, options } = parseUseDynamicSubmitterRestArgs(rest);\n\tconst keySuffix = options.keySuffix;\n\n\tconst url = useMemo(() => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Intentional\n\t\treturn href(path, ...(hrefArgs as any));\n\t}, [path, keySuffix, ...(hrefArgs as unknown[])]);\n\n\tconst fetcherKey = useMemo(\n\t\t() => dynamicSubmitterFetcherKey(url, keySuffix),\n\t\t[url, keySuffix],\n\t);\n\n\tconst fetcher = useFetcher<TInfo[\"action\"]>({\n\t\tkey: fetcherKey,\n\t});\n\n\tconst fetcherRef = useRef(fetcher);\n\tfetcherRef.current = fetcher;\n\n\tconst ownerIdRef = useRef(allocateSubmitterOwnerId());\n\tconst prevStateRef = useRef(fetcher.state);\n\n\tconst beginSubmit = useCallback(\n\t\t(runSubmit: () => void) => {\n\t\t\treturn new Promise<DynamicSubmitterData<TInfo>>((resolve, reject) => {\n\t\t\t\tconst bucket = getSubmitterKeyBucket(fetcherKey);\n\t\t\t\tconst prevPending = bucket.pending;\n\t\t\t\tif (prevPending) {\n\t\t\t\t\tprevPending.reject(new SubmitterSupersededError());\n\t\t\t\t}\n\t\t\t\tbucket.submitGen += 1;\n\t\t\t\tconst gen = bucket.submitGen;\n\t\t\t\tbucket.pending = {\n\t\t\t\t\tgen,\n\t\t\t\t\townerId: ownerIdRef.current,\n\t\t\t\t\treject,\n\t\t\t\t\tfinishIdle: (data, error) => {\n\t\t\t\t\t\tif (data !== undefined) {\n\t\t\t\t\t\t\tresolve(data as DynamicSubmitterData<TInfo>);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treject(error ?? new Error(\"Submission failed\"));\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t\trunSubmit();\n\t\t\t});\n\t\t},\n\t\t[fetcherKey],\n\t);\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tconst bucket = getSubmitterKeyBucket(fetcherKey);\n\t\t\tconst pending = bucket.pending;\n\t\t\tif (pending && pending.ownerId === ownerIdRef.current) {\n\t\t\t\tbucket.pending = null;\n\t\t\t\tpending.reject(new SubmitterUnmountedError());\n\t\t\t}\n\t\t};\n\t}, [fetcherKey]);\n\n\tconst fetcherError = (fetcher as { error?: unknown }).error;\n\n\tuseEffect(() => {\n\t\tconst prev = prevStateRef.current;\n\t\tprevStateRef.current = fetcher.state;\n\t\tconst wasWorking = prev === \"submitting\" || prev === \"loading\";\n\t\tif (!wasWorking || fetcher.state !== \"idle\") {\n\t\t\treturn;\n\t\t}\n\t\tconst bucket = getSubmitterKeyBucket(fetcherKey);\n\t\tconst p = bucket.pending;\n\t\tif (!p || p.gen !== bucket.submitGen) {\n\t\t\treturn;\n\t\t}\n\t\tbucket.pending = null;\n\t\tp.finishIdle(fetcher.data, fetcherError);\n\t}, [fetcherKey, fetcher.state, fetcher.data, fetcherError]);\n\n\tconst submit: SubmitFunc<TInfo> = useCallback(\n\t\t(target, options) => {\n\t\t\treturn beginSubmit(() => {\n\t\t\t\tconst f = fetcherRef.current;\n\t\t\t\tvoid f.submit(target, {\n\t\t\t\t\t...options,\n\t\t\t\t\tmethod: (options?.method ?? \"POST\") as HTMLFormMethod,\n\t\t\t\t\taction: url,\n\t\t\t\t\tencType: \"multipart/form-data\",\n\t\t\t\t} as Parameters<typeof f.submit>[1]);\n\t\t\t});\n\t\t},\n\t\t[beginSubmit, url],\n\t);\n\n\tconst submitJson: SubmitJsonFunc<TInfo> = useCallback(\n\t\t(data, options = {}) => {\n\t\t\treturn beginSubmit(() => {\n\t\t\t\tconst f = fetcherRef.current;\n\t\t\t\tvoid f.submit(\n\t\t\t\t\tdata as SubmitTarget,\n\t\t\t\t\t{\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\tmethod: (options.method ?? \"POST\") as HTMLFormMethod,\n\t\t\t\t\t\taction: url,\n\t\t\t\t\t\tencType: \"application/json\",\n\t\t\t\t\t} as Parameters<typeof f.submit>[1],\n\t\t\t\t);\n\t\t\t});\n\t\t},\n\t\t[beginSubmit, url],\n\t);\n\n\tconst fetcherFormRef = useRef(fetcher.Form);\n\tfetcherFormRef.current = fetcher.Form;\n\n\tconst Form: SubmitForm = useCallback(\n\t\t({ method = \"POST\", ...props }) => {\n\t\t\tconst OriginalForm = fetcherFormRef.current;\n\t\t\treturn <OriginalForm action={url} method={method} {...props} />;\n\t\t},\n\t\t[url],\n\t);\n\n\treturn useMemo(\n\t\t() => ({\n\t\t\tsubmit,\n\t\t\tsubmitJson,\n\t\t\tForm,\n\t\t\tfetcherKey,\n\t\t}),\n\t\t[submit, submitJson, Form, fetcherKey],\n\t);\n}\n\n/**\n * React Router `useFetcher` bound to the same key as {@link useDynamicSubmitter}, so `state` /\n * `data` reflect the same submissions as `submitter.submit` / `submitter.Form`.\n *\n * Call at component top level next to `useDynamicSubmitter`.\n */\nexport function useDynamicSubmitterFetcher<TInfo extends RouteWithActionModule>(\n\tsubmitter: UseDynamicSubmitterResult<TInfo>,\n) {\n\treturn useFetcher<TInfo[\"action\"]>({ key: submitter.fetcherKey });\n}\n"]}