@firtoz/router-toolkit 8.0.0 → 9.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.
@@ -0,0 +1 @@
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"]}
@@ -29,5 +29,5 @@ var useDynamicFetcher = (path, ...args) => {
29
29
  };
30
30
 
31
31
  export { useDynamicFetcher };
32
- //# sourceMappingURL=chunk-HX57TC2S.js.map
33
- //# sourceMappingURL=chunk-HX57TC2S.js.map
32
+ //# sourceMappingURL=chunk-F4324Q33.js.map
33
+ //# sourceMappingURL=chunk-F4324Q33.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useDynamicFetcher.ts"],"names":[],"mappings":";;;;AAgNO,IAAM,iBAAA,GAAoB,CAChC,IAAA,EAAA,GACG,IAAA,KAoBC;AACJ,EAAA,MAAM,GAAA,GAAM,QAAQ,MAAM;AAEzB,IAAA,OAAO,IAAA,CAAK,IAAA,EAAM,GAAI,IAAY,CAAA;AAAA,EACnC,CAAA,EAAG,CAAC,IAAA,EAAM,IAAI,CAAC,CAAA;AAEf,EAAA,MAAM,UAAU,UAAA,CAA4B;AAAA,IAC3C,GAAA,EAAK,WAAW,GAAG,CAAA;AAAA,GACnB,CAAA;AAED,EAAA,MAAM,IAAA,GAAO,WAAA;AAAA,IACZ,CAAC,WAAA,KAAyC;AACzC,MAAA,IAAI,CAAC,WAAA,IAAe,MAAA,CAAO,KAAK,WAAW,CAAA,CAAE,WAAW,CAAA,EAAG;AAC1D,QAAA,OAAO,OAAA,CAAQ,KAAK,GAAG,CAAA;AAAA,MACxB;AAGA,MAAA,MAAM,SAAS,IAAI,GAAA,CAAI,GAAA,EAAK,MAAA,CAAO,SAAS,MAAM,CAAA;AAClD,MAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACvD,QAAA,MAAA,CAAO,YAAA,CAAa,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,MACnC;AAEA,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,QAAA,GAAW,OAAO,MAAM,CAAA;AAAA,IACpD,CAAA;AAAA,IACA,CAAC,OAAA,CAAQ,IAAA,EAAM,GAAG;AAAA,GACnB;AAEA,EAAA,OAAO;AAAA,IACN,GAAG,OAAA;AAAA,IACH;AAAA,GACD;AACD","file":"chunk-F4324Q33.js","sourcesContent":["/**\n * @fileoverview Type-safe dynamic data fetching hook for React Router 7\n *\n * This module provides a hook that creates a type-safe fetcher for loading data\n * from dynamic routes with full TypeScript inference for the loader response and route params.\n *\n * @example\n * ### Route Setup (`app/routes/api.users.$userId.ts`)\n *\n * First, set up your route with the required exports:\n *\n * ```typescript\n * import type { RoutePath } from \"@firtoz/router-toolkit\";\n *\n * // Export the route path for type inference\n * export const route: RoutePath<\"/api/users/:userId\"> = \"/api/users/:userId\";\n *\n * // Define the loader with a typed return value\n * export const loader = async ({ params }: LoaderFunctionArgs) => {\n * const user = await db.users.findUnique({ where: { id: params.userId } });\n * return {\n * user: {\n * id: user.id,\n * email: user.email,\n * displayName: user.displayName,\n * createdAt: user.createdAt.toISOString(),\n * },\n * };\n * };\n * ```\n *\n * @example\n * ### Using the hook in a component\n *\n * ```tsx\n * import { useDynamicFetcher } from \"@firtoz/router-toolkit\";\n * import { useEffect } from \"react\";\n *\n * function UserProfile({ userId }: { userId: string }) {\n * // Type-safe fetcher with full inference\n * const fetcher = useDynamicFetcher<typeof import(\"./api.users.$userId\")>(\n * \"/api/users/:userId\",\n * { userId }\n * );\n *\n * // Load data on mount\n * useEffect(() => {\n * fetcher.load();\n * }, [fetcher.load]);\n *\n * // fetcher.data is fully typed: { user: { id, email, displayName, createdAt } } | undefined\n * if (fetcher.state === \"loading\") {\n * return <div>Loading...</div>;\n * }\n *\n * if (!fetcher.data) {\n * return <div>No user found</div>;\n * }\n *\n * return (\n * <div>\n * <h1>{fetcher.data.user.displayName}</h1>\n * <p>{fetcher.data.user.email}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ### Loading with query parameters\n *\n * ```tsx\n * function SearchResults() {\n * const fetcher = useDynamicFetcher<typeof import(\"./api.search\")>(\"/api/search\");\n *\n * const handleSearch = (query: string, page: number) => {\n * // Pass query params to the load function\n * fetcher.load({ q: query, page: String(page) });\n * };\n *\n * return (\n * <div>\n * <input onChange={(e) => handleSearch(e.target.value, 1)} />\n * {fetcher.data?.results.map((result) => (\n * <div key={result.id}>{result.title}</div>\n * ))}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ### Combining with useDynamicSubmitter for full CRUD\n *\n * You can use `useDynamicFetcher` alongside `useDynamicSubmitter` to create\n * complete CRUD interfaces with type safety:\n *\n * ```tsx\n * import { useDynamicFetcher, useDynamicSubmitter } from \"@firtoz/router-toolkit\";\n * import { useEffect, useState } from \"react\";\n *\n * function PostEditor({ postId }: { postId: string }) {\n * const fetcher = useDynamicFetcher<typeof import(\"./api.posts.$postId\")>(\n * \"/api/posts/:postId\",\n * { postId }\n * );\n *\n * const submitter = useDynamicSubmitter<typeof import(\"./api.posts.$postId\")>(\n * \"/api/posts/:postId\",\n * { postId }\n * );\n *\n * const [saving, setSaving] = useState(false);\n *\n * useEffect(() => {\n * fetcher.load();\n * }, [fetcher.load]);\n *\n * const handleSave = async (title: string, content: string) => {\n * setSaving(true);\n * try {\n * await submitter.submitJson({ title, content }, { method: \"PUT\" });\n * fetcher.load();\n * } finally {\n * setSaving(false);\n * }\n * };\n *\n * if (!fetcher.data) return <div>Loading...</div>;\n *\n * return (\n * <form onSubmit={(e) => {\n * e.preventDefault();\n * const form = new FormData(e.currentTarget);\n * void handleSave(form.get(\"title\") as string, form.get(\"content\") as string);\n * }}>\n * <input name=\"title\" defaultValue={fetcher.data.post.title} />\n * <textarea name=\"content\" defaultValue={fetcher.data.post.content} />\n * <button type=\"submit\" disabled={saving}>\n * {saving ? \"Saving...\" : \"Save\"}\n * </button>\n * </form>\n * );\n * }\n * ```\n *\n * **Submitter UX:** Local `saving` around `await submitter.submitJson` fits a promise-first save +\n * reload flow. For declarative `fetcher.state` / `fetcher.data` in JSX (e.g. with `submitter.Form`),\n * use {@link useDynamicSubmitterFetcher} instead. The package README documents trade-offs under\n * **useDynamicSubmitter** (heading “Local useState vs useDynamicSubmitterFetcher”).\n */\n\nimport { useCallback, useMemo } from \"react\";\nimport { href, useFetcher } from \"react-router\";\nimport type { HrefArgs } from \"./types/HrefArgs\";\nimport type { RouteWithLoaderModule } from \"./types/RouteWithLoaderModule\";\n\n/**\n * Creates a type-safe fetcher for loading data from dynamic routes.\n *\n * This hook provides full TypeScript inference for:\n * - Route parameters (from the route path)\n * - Loader response type (from the route's loader 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 args - Route parameters (if the route has dynamic segments like `:id`)\n *\n * @returns An extended fetcher object with:\n * - `load` - Function to load data, optionally with query parameters\n * - `data` - Response data from the loader (typed)\n * - `state` - Fetcher state (\"idle\" | \"loading\" | \"submitting\")\n * - All other useFetcher properties (except `submit`)\n *\n * @example\n * ### Basic usage\n *\n * ```typescript\n * // In your route file (app/routes/api.products.$productId.ts):\n * export const route: RoutePath<\"/api/products/:productId\"> = \"/api/products/:productId\";\n * export const loader = async ({ params }: LoaderFunctionArgs) => {\n * return { product: await getProduct(params.productId) };\n * };\n *\n * // In your component:\n * const fetcher = useDynamicFetcher<typeof import(\"./api.products.$productId\")>(\n * \"/api/products/:productId\",\n * { productId: \"abc123\" }\n * );\n *\n * useEffect(() => {\n * fetcher.load();\n * }, [fetcher.load]);\n *\n * // fetcher.data is typed as { product: Product } | undefined\n * ```\n *\n * @example\n * ### With query parameters\n *\n * ```typescript\n * const fetcher = useDynamicFetcher<typeof import(\"./api.search\")>(\"/api/search\");\n *\n * // Load with query params: /api/search?q=hello&limit=10\n * fetcher.load({ q: \"hello\", limit: \"10\" });\n * ```\n */\nexport const useDynamicFetcher = <TInfo extends RouteWithLoaderModule>(\n\tpath: TInfo[\"route\"],\n\t...args: TInfo[\"route\"] extends \"undefined\"\n\t\t? HrefArgs<\"/\">\n\t\t: HrefArgs<TInfo[\"route\"]>\n): Omit<ReturnType<typeof useFetcher<TInfo[\"loader\"]>>, \"load\" | \"submit\"> & {\n\t/**\n\t * Load data from the route's loader.\n\t *\n\t * @param queryParams - Optional query parameters to append to the URL\n\t * @returns A promise that resolves when the load is complete\n\t *\n\t * @example\n\t * ```typescript\n\t * // Load without query params\n\t * fetcher.load();\n\t *\n\t * // Load with query params\n\t * fetcher.load({ page: \"2\", sort: \"name\" });\n\t * ```\n\t */\n\tload: (queryParams?: Record<string, string>) => Promise<void>;\n} => {\n\tconst url = useMemo(() => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Intentional\n\t\treturn href(path, ...(args as any));\n\t}, [path, args]);\n\n\tconst fetcher = useFetcher<TInfo[\"loader\"]>({\n\t\tkey: `fetcher-${url}`,\n\t});\n\n\tconst load = useCallback(\n\t\t(queryParams?: Record<string, string>) => {\n\t\t\tif (!queryParams || Object.keys(queryParams).length === 0) {\n\t\t\t\treturn fetcher.load(url);\n\t\t\t}\n\n\t\t\t// Build URL with query parameters\n\t\t\tconst urlObj = new URL(url, window.location.origin);\n\t\t\tfor (const [key, value] of Object.entries(queryParams)) {\n\t\t\t\turlObj.searchParams.set(key, value);\n\t\t\t}\n\n\t\t\treturn fetcher.load(urlObj.pathname + urlObj.search);\n\t\t},\n\t\t[fetcher.load, url],\n\t);\n\n\treturn {\n\t\t...fetcher,\n\t\tload,\n\t};\n};\n"]}
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=chunk-SBJFTOWW.js.map
3
+ //# sourceMappingURL=chunk-SBJFTOWW.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-2RLEUOSR.js","sourcesContent":[]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-SBJFTOWW.js"}
@@ -41,5 +41,5 @@ var formAction = ({
41
41
  };
42
42
 
43
43
  export { formAction };
44
- //# sourceMappingURL=chunk-5MOCOBGV.js.map
45
- //# sourceMappingURL=chunk-5MOCOBGV.js.map
44
+ //# sourceMappingURL=chunk-UF5QHE5K.js.map
45
+ //# sourceMappingURL=chunk-UF5QHE5K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/formAction.ts"],"names":[],"mappings":";;;;;AAuUO,IAAM,aAAa,CAKxB;AAAA,EACD,MAAA;AAAA,EACA;AACD,CAAA,KAA8D;AAC7D,EAAA,OAAO,OACN,IAAA,KACoE;AACpE,IAAA,IAAI;AACH,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,IAAI,cAAc,CAAA;AAC3D,MAAA,MAAM,MAAA,GAAS,WAAA,EAAa,QAAA,CAAS,kBAAkB,CAAA,IAAK,KAAA;AAE5D,MAAA,MAAM,WAAA,GAAc,SACjB,MAAM,MAAA,CAAO,eAAe,MAAM,IAAA,CAAK,QAAQ,IAAA,EAAM,IACrD,MAAM,GAAA,CACL,SAAS,MAAM,CAAA,CACf,eAAe,MAAM,IAAA,CAAK,OAAA,CAAQ,QAAA,EAAU,CAAA;AAEhD,MAAA,IAAI,CAAC,YAAY,OAAA,EAAS;AACzB,QAAA,OAAO,IAAA,CAAK;AAAA,UACX,IAAA,EAAM,YAAA;AAAA,UACN,OAAO,CAAA,CAAE,YAAA;AAAA,YACR,WAAA,CAAY;AAAA;AACb,SACA,CAAA;AAAA,MACF;AAEA,MAAA,MAAM,aAAA,GAAgB,MAAM,OAAA,CAAQ,IAAA,EAAM,YAAY,IAAI,CAAA;AAC1D,MAAA,IAAI,CAAC,cAAc,OAAA,EAAS;AAC3B,QAAA,OAAO,IAAA,CAAK;AAAA,UACX,IAAA,EAAM,SAAA;AAAA,UACN,OAAO,aAAA,CAAc;AAAA,SACrB,CAAA;AAAA,MACF;AAEA,MAAA,OAAO,aAAA;AAAA,IACR,SAAS,KAAA,EAAO;AAEf,MAAA,IAAI,iBAAiB,QAAA,EAAU;AAC9B,QAAA,MAAM,KAAA;AAAA,MACP;AAEA,MAAA,OAAA,CAAQ,KAAA,CAAM,mCAAmC,KAAK,CAAA;AACtD,MAAA,OAAO,IAAA,CAAK;AAAA,QACX,IAAA,EAAM;AAAA,OACN,CAAA;AAAA,IACF;AAAA,EACD,CAAA;AACD","file":"chunk-UF5QHE5K.js","sourcesContent":["/**\n * @fileoverview Type-safe form action utility for React Router 7\n *\n * This module provides a wrapper for React Router actions that handles form data and JSON\n * validation using Zod schemas and provides structured error handling with MaybeError.\n *\n * Supports both:\n * - **JSON requests** (`Content-Type: application/json`) - parsed with `request.json()` and validated directly\n * - **FormData requests** (`multipart/form-data` or `application/x-www-form-urlencoded`) - parsed with `request.formData()` and validated with zod-form-data\n *\n * ## Overview\n *\n * `formAction` is designed to work seamlessly with `useDynamicSubmitter` and `useDynamicFetcher`\n * to provide end-to-end type safety for your React Router forms.\n *\n * @example\n * ### Basic Route Setup (`app/routes/auth.login.tsx`)\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 * // 1. Export the route path for type inference\n * export const route: RoutePath<\"/auth/login\"> = \"/auth/login\";\n *\n * // 2. Define your form schema with Zod\n * export const formSchema = z.object({\n * email: z.string().email(\"Please enter a valid email\"),\n * password: z.string().min(8, \"Password must be at least 8 characters\"),\n * rememberMe: z.boolean().optional().default(false),\n * });\n *\n * // 3. Create the action with formAction\n * export const action = formAction({\n * schema: formSchema,\n * handler: async ({ request }, data) => {\n * // data is fully typed: { email: string, password: string, rememberMe: boolean }\n * try {\n * const user = await authenticateUser(data.email, data.password);\n * if (data.rememberMe) {\n * await createPersistentSession(user.id);\n * }\n * return success({ user });\n * } catch (error) {\n * return fail(\"Invalid email or password\");\n * }\n * },\n * });\n * ```\n *\n * @example\n * ### Using with useDynamicSubmitter\n *\n * The route above can be used with `useDynamicSubmitter` for type-safe form submissions.\n * The hook exposes {@link UseDynamicSubmitterResult.fetcherKey} (built with\n * {@link dynamicSubmitterFetcherKey}) so a parallel `useFetcher` stays aligned; prefer\n * {@link useDynamicSubmitterFetcher} instead of hand-rolling the key.\n *\n * The **optional** {@link useDynamicSubmitterFetcher} below is only for declarative UI that reads\n * `fetcher.state` / `fetcher.data` in render (same submission as `submitter`). For promise-first\n * flows, omit it and use `await submitter.submitJson(...)` plus local `useState` for pending.\n * Use {@link UseDynamicSubmitterOptions.keySuffix} when two submitters target the same URL and\n * must not share fetcher state.\n *\n * ```tsx\n * import {\n * useDynamicSubmitter,\n * useDynamicSubmitterFetcher,\n * } from \"@firtoz/router-toolkit\";\n *\n * function LoginForm() {\n * const submitter = useDynamicSubmitter<typeof import(\"./auth.login\")>(\"/auth/login\");\n * const fetcher = useDynamicSubmitterFetcher(submitter);\n *\n * // Option 1: Submit as JSON (defaults to POST)\n * const handleLoginJson = async () => {\n * await submitter.submitJson({\n * email: \"user@example.com\",\n * password: \"secret123\",\n * rememberMe: true,\n * });\n * };\n *\n * // Option 2: Form + useDynamicSubmitterFetcher for reactive state/data\n * return (\n * <submitter.Form>\n * <input name=\"email\" type=\"email\" placeholder=\"Email\" />\n * <input name=\"password\" type=\"password\" placeholder=\"Password\" />\n * <label>\n * <input name=\"rememberMe\" type=\"checkbox\" /> Remember me\n * </label>\n * <button type=\"submit\" disabled={fetcher.state === \"submitting\"}>\n * {fetcher.state === \"submitting\" ? \"Logging in...\" : \"Login\"}\n * </button>\n *\n * {fetcher.data && !fetcher.data.success && (\n * <div className=\"error\">\n * {fetcher.data.error.type === \"validation\"\n * ? \"Please check your inputs\"\n * : fetcher.data.error.type === \"handler\"\n * ? fetcher.data.error.error\n * : \"An unexpected error occurred\"}\n * </div>\n * )}\n * </submitter.Form>\n * );\n * }\n * ```\n *\n * @example\n * ### Combined loader + action route (`app/routes/admin.posts.$id.tsx`)\n *\n * You can combine `formAction` with a loader for full CRUD operations:\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 * import type { LoaderFunctionArgs } from \"react-router\";\n *\n * export const route: RoutePath<\"/admin/posts/:id\"> = \"/admin/posts/:id\";\n *\n * // Loader for fetching data (used with useDynamicFetcher)\n * export const loader = async ({ params }: LoaderFunctionArgs) => {\n * const post = await db.posts.findUnique({ where: { id: params.id } });\n * return { post };\n * };\n *\n * // Form schema for updates\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 * // Action for handling form submissions (used with useDynamicSubmitter)\n * export const action = formAction({\n * schema: formSchema,\n * handler: async ({ params }, data) => {\n * const updated = await db.posts.update({\n * where: { id: params.id },\n * data,\n * });\n * return success({ post: updated });\n * },\n * });\n * ```\n *\n * @example\n * ### Full CRUD component using both hooks\n *\n * ```tsx\n * import { useDynamicFetcher, useDynamicSubmitter } from \"@firtoz/router-toolkit\";\n * import { useEffect, useState } from \"react\";\n *\n * function PostEditor({ postId }: { postId: string }) {\n * const fetcher = useDynamicFetcher<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\n *\n * const submitter = useDynamicSubmitter<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\n *\n * const [saving, setSaving] = useState(false);\n *\n * useEffect(() => {\n * fetcher.load();\n * }, [fetcher.load]);\n *\n * if (fetcher.state === \"loading\" && !fetcher.data) {\n * return <div>Loading...</div>;\n * }\n *\n * const post = fetcher.data?.post;\n *\n * return (\n * <submitter.Form\n * method=\"PUT\"\n * onSubmit={async (e) => {\n * e.preventDefault();\n * setSaving(true);\n * try {\n * const fd = new FormData(e.currentTarget);\n * await submitter.submit(fd, { method: \"PUT\" });\n * fetcher.load();\n * } finally {\n * setSaving(false);\n * }\n * }}\n * >\n * <input name=\"title\" defaultValue={post?.title} />\n * <textarea name=\"content\" defaultValue={post?.content} />\n * <label>\n * <input name=\"published\" type=\"checkbox\" defaultChecked={post?.published} />\n * Published\n * </label>\n * <button type=\"submit\" disabled={saving}>\n * {saving ? \"Saving...\" : \"Save\"}\n * </button>\n * </submitter.Form>\n * );\n * }\n * ```\n */\n\nimport { fail, type MaybeError } from \"@firtoz/maybe-error\";\nimport type { ActionFunctionArgs } from \"react-router\";\nimport { z } from \"zod\";\nimport { zfd } from \"zod-form-data\";\n\n/**\n * Error types that can be returned by formAction\n */\nexport type FormActionError<TError, TSchema extends z.ZodTypeAny> =\n\t| {\n\t\t\ttype: \"validation\";\n\t\t\terror: ReturnType<typeof z.treeifyError<z.infer<TSchema>>>;\n\t }\n\t| {\n\t\t\ttype: \"handler\";\n\t\t\terror: TError;\n\t }\n\t| {\n\t\t\ttype: \"unknown\";\n\t };\n\n/**\n * Configuration object for formAction\n *\n * @template TSchema - The Zod schema type for form validation\n * @template TResult - The success result type from the handler\n * @template TError - The error type that the handler can return\n * @template ActionArgs - The action function arguments type (defaults to ActionFunctionArgs)\n */\nexport interface FormActionConfig<\n\tTSchema extends z.ZodTypeAny,\n\tTResult = undefined,\n\tTError = string,\n\tActionArgs extends ActionFunctionArgs = ActionFunctionArgs,\n> {\n\t/**\n\t * Zod schema to validate the form data against\n\t */\n\tschema: TSchema;\n\t/**\n\t * Handler function that processes the validated form data\n\t *\n\t * @param args - The original action function arguments\n\t * @param data - The validated form data (typed according to the schema)\n\t * @returns A promise that resolves to a MaybeError with the result or error\n\t */\n\thandler: (\n\t\targs: ActionArgs,\n\t\tdata: z.infer<TSchema>,\n\t) => Promise<MaybeError<TResult, TError>>;\n}\n\n/**\n * Creates a type-safe form action handler that validates form data or JSON and provides structured error handling.\n *\n * This function wraps a React Router action to:\n * 1. Detect content type (JSON vs FormData) from the request headers\n * 2. Parse and validate the request body using a Zod schema\n * 3. Call the provided handler with validated data\n * 4. Return structured errors for validation failures, handler errors, or unknown errors\n * 5. Preserve React Router Response objects (redirects, etc.) by re-throwing them\n *\n * **Content-Type handling:**\n * - `application/json`: Uses `request.json()` and validates directly with the schema\n * - `multipart/form-data` or `application/x-www-form-urlencoded`: Uses `request.formData()` and validates with zod-form-data\n *\n * @template TSchema - The Zod schema type for form validation\n * @template TResult - The success result type from the handler (defaults to undefined)\n * @template TError - The error type that the handler can return (defaults to string)\n * @template ActionArgs - The action function arguments type (defaults to ActionFunctionArgs)\n *\n * @param config - Configuration object containing schema and handler\n * @returns An action function that can be used with React Router\n *\n * @example\n * ```typescript\n * import { z } from \"zod\";\n * import { formAction } from \"@firtoz/router-toolkit\";\n * import { success, fail } from \"@firtoz/maybe-error\";\n *\n * const loginSchema = z.object({\n * email: z.string().email(\"Invalid email format\"),\n * password: z.string().min(8, \"Password must be at least 8 characters\"),\n * });\n *\n * export const action = formAction({\n * schema: loginSchema,\n * handler: async (args, data) => {\n * try {\n * const user = await authenticateUser(data.email, data.password);\n * return success(user);\n * } catch (error) {\n * return fail(\"Invalid credentials\");\n * }\n * },\n * });\n * ```\n *\n * @example\n * ```typescript\n * // In your component, handle the different error types:\n * const actionData = useActionData<typeof action>();\n *\n * if (actionData && !actionData.success) {\n * switch (actionData.error.type) {\n * case \"validation\":\n * // Handle validation errors - actionData.error.error contains field-specific errors\n * break;\n * case \"handler\":\n * // Handle business logic errors - actionData.error.error contains your custom error\n * break;\n * case \"unknown\":\n * // Handle unexpected errors\n * break;\n * }\n * }\n * ```\n */\nexport const formAction = <\n\tTSchema extends z.ZodTypeAny,\n\tTResult = undefined,\n\tTError = string,\n\tActionArgs extends ActionFunctionArgs = ActionFunctionArgs,\n>({\n\tschema,\n\thandler,\n}: FormActionConfig<TSchema, TResult, TError, ActionArgs>) => {\n\treturn async (\n\t\targs: ActionArgs,\n\t): Promise<MaybeError<TResult, FormActionError<TError, TSchema>>> => {\n\t\ttry {\n\t\t\tconst contentType = args.request.headers.get(\"Content-Type\");\n\t\t\tconst isJson = contentType?.includes(\"application/json\") ?? false;\n\n\t\t\tconst parseResult = isJson\n\t\t\t\t? await schema.safeParseAsync(await args.request.json())\n\t\t\t\t: await zfd\n\t\t\t\t\t\t.formData(schema)\n\t\t\t\t\t\t.safeParseAsync(await args.request.formData());\n\n\t\t\tif (!parseResult.success) {\n\t\t\t\treturn fail({\n\t\t\t\t\ttype: \"validation\" as const,\n\t\t\t\t\terror: z.treeifyError<z.infer<TSchema>>(\n\t\t\t\t\t\tparseResult.error as z.core.$ZodError<z.infer<TSchema>>,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst handlerResult = await handler(args, parseResult.data);\n\t\t\tif (!handlerResult.success) {\n\t\t\t\treturn fail({\n\t\t\t\t\ttype: \"handler\" as const,\n\t\t\t\t\terror: handlerResult.error,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn handlerResult;\n\t\t} catch (error) {\n\t\t\t// Re-throw Response objects (redirects, etc.) to preserve React Router behavior\n\t\t\tif (error instanceof Response) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tconsole.error(\"Unexpected error in formAction:\", error);\n\t\t\treturn fail({\n\t\t\t\ttype: \"unknown\" as const,\n\t\t\t});\n\t\t}\n\t};\n};\n"]}
@@ -56,13 +56,26 @@ import { z } from 'zod';
56
56
  * @example
57
57
  * ### Using with useDynamicSubmitter
58
58
  *
59
- * The route above can be used with `useDynamicSubmitter` for type-safe form submissions:
59
+ * The route above can be used with `useDynamicSubmitter` for type-safe form submissions.
60
+ * The hook exposes {@link UseDynamicSubmitterResult.fetcherKey} (built with
61
+ * {@link dynamicSubmitterFetcherKey}) so a parallel `useFetcher` stays aligned; prefer
62
+ * {@link useDynamicSubmitterFetcher} instead of hand-rolling the key.
63
+ *
64
+ * The **optional** {@link useDynamicSubmitterFetcher} below is only for declarative UI that reads
65
+ * `fetcher.state` / `fetcher.data` in render (same submission as `submitter`). For promise-first
66
+ * flows, omit it and use `await submitter.submitJson(...)` plus local `useState` for pending.
67
+ * Use {@link UseDynamicSubmitterOptions.keySuffix} when two submitters target the same URL and
68
+ * must not share fetcher state.
60
69
  *
61
70
  * ```tsx
62
- * import { useDynamicSubmitter } from "@firtoz/router-toolkit";
71
+ * import {
72
+ * useDynamicSubmitter,
73
+ * useDynamicSubmitterFetcher,
74
+ * } from "@firtoz/router-toolkit";
63
75
  *
64
76
  * function LoginForm() {
65
77
  * const submitter = useDynamicSubmitter<typeof import("./auth.login")>("/auth/login");
78
+ * const fetcher = useDynamicSubmitterFetcher(submitter);
66
79
  *
67
80
  * // Option 1: Submit as JSON (defaults to POST)
68
81
  * const handleLoginJson = async () => {
@@ -73,7 +86,7 @@ import { z } from 'zod';
73
86
  * });
74
87
  * };
75
88
  *
76
- * // Option 2: Use the Form component (defaults to POST)
89
+ * // Option 2: Form + useDynamicSubmitterFetcher for reactive state/data
77
90
  * return (
78
91
  * <submitter.Form>
79
92
  * <input name="email" type="email" placeholder="Email" />
@@ -81,16 +94,16 @@ import { z } from 'zod';
81
94
  * <label>
82
95
  * <input name="rememberMe" type="checkbox" /> Remember me
83
96
  * </label>
84
- * <button disabled={submitter.state !== "idle"}>
85
- * {submitter.state === "submitting" ? "Logging in..." : "Login"}
97
+ * <button type="submit" disabled={fetcher.state === "submitting"}>
98
+ * {fetcher.state === "submitting" ? "Logging in..." : "Login"}
86
99
  * </button>
87
100
  *
88
- * {submitter.data && !submitter.data.success && (
101
+ * {fetcher.data && !fetcher.data.success && (
89
102
  * <div className="error">
90
- * {submitter.data.error.type === "validation"
103
+ * {fetcher.data.error.type === "validation"
91
104
  * ? "Please check your inputs"
92
- * : submitter.data.error.type === "handler"
93
- * ? submitter.data.error.error // "Invalid email or password"
105
+ * : fetcher.data.error.type === "handler"
106
+ * ? fetcher.data.error.error
94
107
  * : "An unexpected error occurred"}
95
108
  * </div>
96
109
  * )}
@@ -143,21 +156,21 @@ import { z } from 'zod';
143
156
  *
144
157
  * ```tsx
145
158
  * import { useDynamicFetcher, useDynamicSubmitter } from "@firtoz/router-toolkit";
146
- * import { useEffect } from "react";
159
+ * import { useEffect, useState } from "react";
147
160
  *
148
161
  * function PostEditor({ postId }: { postId: string }) {
149
- * // Fetch post data
150
162
  * const fetcher = useDynamicFetcher<typeof import("./admin.posts.$id")>(
151
163
  * "/admin/posts/:id",
152
164
  * { id: postId }
153
165
  * );
154
166
  *
155
- * // Submit updates
156
167
  * const submitter = useDynamicSubmitter<typeof import("./admin.posts.$id")>(
157
168
  * "/admin/posts/:id",
158
169
  * { id: postId }
159
170
  * );
160
171
  *
172
+ * const [saving, setSaving] = useState(false);
173
+ *
161
174
  * useEffect(() => {
162
175
  * fetcher.load();
163
176
  * }, [fetcher.load]);
@@ -169,15 +182,28 @@ import { z } from 'zod';
169
182
  * const post = fetcher.data?.post;
170
183
  *
171
184
  * return (
172
- * <submitter.Form method="PUT">
185
+ * <submitter.Form
186
+ * method="PUT"
187
+ * onSubmit={async (e) => {
188
+ * e.preventDefault();
189
+ * setSaving(true);
190
+ * try {
191
+ * const fd = new FormData(e.currentTarget);
192
+ * await submitter.submit(fd, { method: "PUT" });
193
+ * fetcher.load();
194
+ * } finally {
195
+ * setSaving(false);
196
+ * }
197
+ * }}
198
+ * >
173
199
  * <input name="title" defaultValue={post?.title} />
174
200
  * <textarea name="content" defaultValue={post?.content} />
175
201
  * <label>
176
202
  * <input name="published" type="checkbox" defaultChecked={post?.published} />
177
203
  * Published
178
204
  * </label>
179
- * <button disabled={submitter.state !== "idle"}>
180
- * {submitter.state === "submitting" ? "Saving..." : "Save"}
205
+ * <button type="submit" disabled={saving}>
206
+ * {saving ? "Saving..." : "Save"}
181
207
  * </button>
182
208
  * </submitter.Form>
183
209
  * );
@@ -1,3 +1,3 @@
1
- export { formAction } from './chunk-5MOCOBGV.js';
1
+ export { formAction } from './chunk-UF5QHE5K.js';
2
2
  //# sourceMappingURL=formAction.js.map
3
3
  //# sourceMappingURL=formAction.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider, FormDataSubmittedData, Operation, OperationStatus, SubmitFormDataOptions, SubmitJsonOptions, SubmitJsonResult } from './ConcurrentSubmitterProvider.js';
2
2
  export { FormActionConfig, FormActionError, formAction } from './formAction.js';
3
- export * from '@firtoz/maybe-error';
4
3
  export { Func } from './types/Func.js';
5
4
  export { HrefArgs } from './types/HrefArgs.js';
6
5
  export { RegisterPages } from './types/RegisterPages.js';
@@ -10,9 +9,10 @@ export { RouteWithLoaderModule } from './types/RouteWithLoaderModule.js';
10
9
  export { useCachedFetch } from './useCachedFetch.js';
11
10
  export { UseConcurrentSubmitterReturn, useConcurrentSubmitter } from './useConcurrentSubmitter.js';
12
11
  export { useDynamicFetcher } from './useDynamicFetcher.js';
13
- export { useDynamicSubmitter } from './useDynamicSubmitter.js';
12
+ export { DynamicSubmitterData, SubmitterSupersededError, SubmitterUnmountedError, UseDynamicSubmitterOptions, UseDynamicSubmitterResult, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './useDynamicSubmitter.js';
14
13
  export { useFetcherStateChanged } from './useFetcherStateChanged.js';
15
14
  import 'react/jsx-runtime';
16
15
  import 'react';
16
+ import '@firtoz/maybe-error';
17
17
  import 'react-router';
18
18
  import 'zod';
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import './chunk-2RLEUOSR.js';
1
+ import './chunk-SBJFTOWW.js';
2
2
  import './chunk-2W5QFQTE.js';
3
3
  export { useCachedFetch } from './chunk-OQGTU34H.js';
4
4
  export { useConcurrentSubmitter } from './chunk-QZDEBX3D.js';
5
- export { useDynamicFetcher } from './chunk-HX57TC2S.js';
6
- export { useDynamicSubmitter } from './chunk-JJN6GBJL.js';
5
+ export { useDynamicFetcher } from './chunk-F4324Q33.js';
6
+ export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './chunk-2WSA75KM.js';
7
7
  export { useFetcherStateChanged } from './chunk-W5R7YYHR.js';
8
8
  export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider } from './chunk-ZC6U3MIB.js';
9
- export { formAction } from './chunk-5MOCOBGV.js';
9
+ export { formAction } from './chunk-UF5QHE5K.js';
10
10
  import './chunk-YYJDSKJG.js';
11
11
  import './chunk-ZMRGBYFH.js';
12
12
  import './chunk-MKH4ZP5U.js';
@@ -1,4 +1,3 @@
1
- export * from '@firtoz/maybe-error';
2
1
  export { Func } from './Func.js';
3
2
  export { HrefArgs } from './HrefArgs.js';
4
3
  export { RegisterPages } from './RegisterPages.js';
@@ -1,4 +1,4 @@
1
- import '../chunk-2RLEUOSR.js';
1
+ import '../chunk-SBJFTOWW.js';
2
2
  import '../chunk-2W5QFQTE.js';
3
3
  import '../chunk-YYJDSKJG.js';
4
4
  import '../chunk-ZMRGBYFH.js';
@@ -103,28 +103,33 @@ import './types/Func.js';
103
103
  *
104
104
  * ```tsx
105
105
  * import { useDynamicFetcher, useDynamicSubmitter } from "@firtoz/router-toolkit";
106
+ * import { useEffect, useState } from "react";
106
107
  *
107
108
  * function PostEditor({ postId }: { postId: string }) {
108
- * // Fetch post data
109
109
  * const fetcher = useDynamicFetcher<typeof import("./api.posts.$postId")>(
110
110
  * "/api/posts/:postId",
111
111
  * { postId }
112
112
  * );
113
113
  *
114
- * // Submit updates
115
114
  * const submitter = useDynamicSubmitter<typeof import("./api.posts.$postId")>(
116
115
  * "/api/posts/:postId",
117
116
  * { postId }
118
117
  * );
119
118
  *
119
+ * const [saving, setSaving] = useState(false);
120
+ *
120
121
  * useEffect(() => {
121
122
  * fetcher.load();
122
123
  * }, [fetcher.load]);
123
124
  *
124
125
  * const handleSave = async (title: string, content: string) => {
125
- * await submitter.submitJson({ title, content }, { method: "PUT" });
126
- * // Reload after save
127
- * fetcher.load();
126
+ * setSaving(true);
127
+ * try {
128
+ * await submitter.submitJson({ title, content }, { method: "PUT" });
129
+ * fetcher.load();
130
+ * } finally {
131
+ * setSaving(false);
132
+ * }
128
133
  * };
129
134
  *
130
135
  * if (!fetcher.data) return <div>Loading...</div>;
@@ -133,17 +138,22 @@ import './types/Func.js';
133
138
  * <form onSubmit={(e) => {
134
139
  * e.preventDefault();
135
140
  * const form = new FormData(e.currentTarget);
136
- * handleSave(form.get("title") as string, form.get("content") as string);
141
+ * void handleSave(form.get("title") as string, form.get("content") as string);
137
142
  * }}>
138
143
  * <input name="title" defaultValue={fetcher.data.post.title} />
139
144
  * <textarea name="content" defaultValue={fetcher.data.post.content} />
140
- * <button disabled={submitter.state !== "idle"}>
141
- * {submitter.state === "submitting" ? "Saving..." : "Save"}
145
+ * <button type="submit" disabled={saving}>
146
+ * {saving ? "Saving..." : "Save"}
142
147
  * </button>
143
148
  * </form>
144
149
  * );
145
150
  * }
146
151
  * ```
152
+ *
153
+ * **Submitter UX:** Local `saving` around `await submitter.submitJson` fits a promise-first save +
154
+ * reload flow. For declarative `fetcher.state` / `fetcher.data` in JSX (e.g. with `submitter.Form`),
155
+ * use {@link useDynamicSubmitterFetcher} instead. The package README documents trade-offs under
156
+ * **useDynamicSubmitter** (heading “Local useState vs useDynamicSubmitterFetcher”).
147
157
  */
148
158
 
149
159
  /**
@@ -1,3 +1,3 @@
1
- export { useDynamicFetcher } from './chunk-HX57TC2S.js';
1
+ export { useDynamicFetcher } from './chunk-F4324Q33.js';
2
2
  //# sourceMappingURL=useDynamicFetcher.js.map
3
3
  //# sourceMappingURL=useDynamicFetcher.js.map