@firtoz/router-toolkit 7.0.2 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/ConcurrentSubmitterProvider.d.ts +41 -0
- package/dist/ConcurrentSubmitterProvider.js +3 -0
- package/dist/ConcurrentSubmitterProvider.js.map +1 -0
- package/dist/chunk-2RLEUOSR.js +3 -0
- package/dist/chunk-2RLEUOSR.js.map +1 -0
- package/dist/chunk-2W5QFQTE.js +3 -0
- package/dist/chunk-2W5QFQTE.js.map +1 -0
- package/dist/chunk-5MOCOBGV.js +45 -0
- package/dist/chunk-5MOCOBGV.js.map +1 -0
- package/dist/chunk-HX57TC2S.js +33 -0
- package/dist/chunk-HX57TC2S.js.map +1 -0
- package/dist/chunk-JJN6GBJL.js +55 -0
- package/dist/chunk-JJN6GBJL.js.map +1 -0
- package/dist/chunk-MKH4ZP5U.js +3 -0
- package/dist/chunk-MKH4ZP5U.js.map +1 -0
- package/dist/chunk-OQGTU34H.js +44 -0
- package/dist/chunk-OQGTU34H.js.map +1 -0
- package/dist/chunk-QZDEBX3D.js +71 -0
- package/dist/chunk-QZDEBX3D.js.map +1 -0
- package/dist/chunk-R6HEJ5E5.js +3 -0
- package/dist/chunk-R6HEJ5E5.js.map +1 -0
- package/dist/chunk-S4QCJFVR.js +3 -0
- package/dist/chunk-S4QCJFVR.js.map +1 -0
- package/dist/chunk-W5R7YYHR.js +16 -0
- package/dist/chunk-W5R7YYHR.js.map +1 -0
- package/dist/chunk-YYJDSKJG.js +3 -0
- package/dist/chunk-YYJDSKJG.js.map +1 -0
- package/dist/chunk-ZC6U3MIB.js +189 -0
- package/dist/chunk-ZC6U3MIB.js.map +1 -0
- package/dist/chunk-ZMRGBYFH.js +3 -0
- package/dist/chunk-ZMRGBYFH.js.map +1 -0
- package/dist/formAction.d.ts +290 -0
- package/dist/formAction.js +3 -0
- package/dist/formAction.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/types/Func.d.ts +3 -0
- package/dist/types/Func.js +3 -0
- package/dist/types/Func.js.map +1 -0
- package/dist/types/HrefArgs.d.ts +8 -0
- package/dist/types/HrefArgs.js +3 -0
- package/dist/types/HrefArgs.js.map +1 -0
- package/dist/types/RegisterPages.d.ts +11 -0
- package/dist/types/RegisterPages.js +3 -0
- package/dist/types/RegisterPages.js.map +1 -0
- package/dist/types/RoutePath.d.ts +6 -0
- package/dist/types/RoutePath.js +3 -0
- package/dist/types/RoutePath.js.map +1 -0
- package/dist/types/RouteWithActionModule.d.ts +18 -0
- package/dist/types/RouteWithActionModule.js +3 -0
- package/dist/types/RouteWithActionModule.js.map +1 -0
- package/dist/types/RouteWithLoaderModule.d.ts +10 -0
- package/dist/types/RouteWithLoaderModule.js +3 -0
- package/dist/types/RouteWithLoaderModule.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/useCachedFetch.d.ts +13 -0
- package/dist/useCachedFetch.js +3 -0
- package/dist/useCachedFetch.js.map +1 -0
- package/dist/useConcurrentSubmitter.d.ts +34 -0
- package/dist/useConcurrentSubmitter.js +4 -0
- package/dist/useConcurrentSubmitter.js.map +1 -0
- package/dist/useDynamicFetcher.d.ts +219 -0
- package/dist/useDynamicFetcher.js +3 -0
- package/dist/useDynamicFetcher.js.map +1 -0
- package/dist/useDynamicSubmitter.d.ts +256 -0
- package/dist/useDynamicSubmitter.js +3 -0
- package/dist/useDynamicSubmitter.js.map +1 -0
- package/dist/useFetcherStateChanged.d.ts +10 -0
- package/dist/useFetcherStateChanged.js +3 -0
- package/dist/useFetcherStateChanged.js.map +1 -0
- package/package.json +21 -12
package/README.md
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@firtoz/router-toolkit)
|
|
5
5
|
[](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://react.dev)
|
|
9
|
+
[](https://reactrouter.com)
|
|
10
|
+
|
|
11
|
+
**React Router 7 framework mode helpers** — typed fetchers, submitters, concurrent uploads, and Zod form actions wired to your route modules.
|
|
8
12
|
|
|
9
13
|
> **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
|
|
10
14
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/** Status of a single concurrent submission */
|
|
5
|
+
type OperationStatus = "pending" | "done" | "error";
|
|
6
|
+
/** One tracked submission: pending → done (or error). Includes submitted payload for optimistic UI. */
|
|
7
|
+
type Operation<TResponse = unknown, TFormData = unknown> = {
|
|
8
|
+
id: string;
|
|
9
|
+
status: OperationStatus;
|
|
10
|
+
/** Data that was sent (for optimistic display while pending, or to show what was submitted) */
|
|
11
|
+
submittedData: TFormData;
|
|
12
|
+
/** Response from the action when status is "done" */
|
|
13
|
+
data?: TResponse;
|
|
14
|
+
error?: unknown;
|
|
15
|
+
};
|
|
16
|
+
/** Result of submitJson / submitFormData: id to look up in operations, promise to await */
|
|
17
|
+
type SubmitJsonResult<T> = {
|
|
18
|
+
id: string;
|
|
19
|
+
promise: Promise<T>;
|
|
20
|
+
};
|
|
21
|
+
/** Optional serializable payload for FormData submissions (for display in operations list). */
|
|
22
|
+
type FormDataSubmittedData = Record<string, unknown>;
|
|
23
|
+
type SubmitJsonOptions = {
|
|
24
|
+
method?: "POST" | "PUT" | "PATCH" | "DELETE";
|
|
25
|
+
};
|
|
26
|
+
type SubmitFormDataOptions = {
|
|
27
|
+
headers?: HeadersInit;
|
|
28
|
+
method?: "POST" | "PUT" | "PATCH" | "DELETE";
|
|
29
|
+
};
|
|
30
|
+
type ContextValue = {
|
|
31
|
+
operations: Record<string, Operation<unknown, unknown>>;
|
|
32
|
+
addJsonSubmission: (path: string, args: Record<string, string> | undefined, data: unknown, options?: SubmitJsonOptions) => SubmitJsonResult<unknown>;
|
|
33
|
+
addFormSubmission: (path: string, args: Record<string, string> | undefined, formData: FormData, submittedData: FormDataSubmittedData, options?: SubmitFormDataOptions) => SubmitJsonResult<unknown>;
|
|
34
|
+
onSettle: (id: string, data?: unknown, error?: unknown) => void;
|
|
35
|
+
};
|
|
36
|
+
declare const ConcurrentSubmitterContext: React.Context<ContextValue | null>;
|
|
37
|
+
declare function ConcurrentSubmitterProvider({ children, }: {
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
}): react_jsx_runtime.JSX.Element;
|
|
40
|
+
|
|
41
|
+
export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider, type FormDataSubmittedData, type Operation, type OperationStatus, type SubmitFormDataOptions, type SubmitJsonOptions, type SubmitJsonResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"ConcurrentSubmitterProvider.js"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-2RLEUOSR.js","sourcesContent":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-2W5QFQTE.js"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fail } from '@firtoz/maybe-error';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { zfd } from 'zod-form-data';
|
|
4
|
+
|
|
5
|
+
// src/formAction.ts
|
|
6
|
+
var formAction = ({
|
|
7
|
+
schema,
|
|
8
|
+
handler
|
|
9
|
+
}) => {
|
|
10
|
+
return async (args) => {
|
|
11
|
+
try {
|
|
12
|
+
const contentType = args.request.headers.get("Content-Type");
|
|
13
|
+
const isJson = contentType?.includes("application/json") ?? false;
|
|
14
|
+
const parseResult = isJson ? await schema.safeParseAsync(await args.request.json()) : await zfd.formData(schema).safeParseAsync(await args.request.formData());
|
|
15
|
+
if (!parseResult.success) {
|
|
16
|
+
return fail({
|
|
17
|
+
type: "validation",
|
|
18
|
+
error: z.treeifyError(
|
|
19
|
+
parseResult.error
|
|
20
|
+
)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const handlerResult = await handler(args, parseResult.data);
|
|
24
|
+
if (!handlerResult.success) {
|
|
25
|
+
return fail({
|
|
26
|
+
type: "handler",
|
|
27
|
+
error: handlerResult.error
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return handlerResult;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof Response) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
console.error("Unexpected error in formAction:", error);
|
|
36
|
+
return fail({
|
|
37
|
+
type: "unknown"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { formAction };
|
|
44
|
+
//# sourceMappingURL=chunk-5MOCOBGV.js.map
|
|
45
|
+
//# sourceMappingURL=chunk-5MOCOBGV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/formAction.ts"],"names":[],"mappings":";;;;;AA6SO,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-5MOCOBGV.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 *\n * ```tsx\n * import { useDynamicSubmitter } from \"@firtoz/router-toolkit\";\n *\n * function LoginForm() {\n * const submitter = useDynamicSubmitter<typeof import(\"./auth.login\")>(\"/auth/login\");\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: Use the Form component (defaults to POST)\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 disabled={submitter.state !== \"idle\"}>\n * {submitter.state === \"submitting\" ? \"Logging in...\" : \"Login\"}\n * </button>\n *\n * {submitter.data && !submitter.data.success && (\n * <div className=\"error\">\n * {submitter.data.error.type === \"validation\"\n * ? \"Please check your inputs\"\n * : submitter.data.error.type === \"handler\"\n * ? submitter.data.error.error // \"Invalid email or password\"\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 } from \"react\";\n *\n * function PostEditor({ postId }: { postId: string }) {\n * // Fetch post data\n * const fetcher = useDynamicFetcher<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\n *\n * // Submit updates\n * const submitter = useDynamicSubmitter<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\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 method=\"PUT\">\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 disabled={submitter.state !== \"idle\"}>\n * {submitter.state === \"submitting\" ? \"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"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo, useCallback } from 'react';
|
|
2
|
+
import { href, useFetcher } from 'react-router';
|
|
3
|
+
|
|
4
|
+
// src/useDynamicFetcher.ts
|
|
5
|
+
var useDynamicFetcher = (path, ...args) => {
|
|
6
|
+
const url = useMemo(() => {
|
|
7
|
+
return href(path, ...args);
|
|
8
|
+
}, [path, args]);
|
|
9
|
+
const fetcher = useFetcher({
|
|
10
|
+
key: `fetcher-${url}`
|
|
11
|
+
});
|
|
12
|
+
const load = useCallback(
|
|
13
|
+
(queryParams) => {
|
|
14
|
+
if (!queryParams || Object.keys(queryParams).length === 0) {
|
|
15
|
+
return fetcher.load(url);
|
|
16
|
+
}
|
|
17
|
+
const urlObj = new URL(url, window.location.origin);
|
|
18
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
19
|
+
urlObj.searchParams.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
return fetcher.load(urlObj.pathname + urlObj.search);
|
|
22
|
+
},
|
|
23
|
+
[fetcher.load, url]
|
|
24
|
+
);
|
|
25
|
+
return {
|
|
26
|
+
...fetcher,
|
|
27
|
+
load
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { useDynamicFetcher };
|
|
32
|
+
//# sourceMappingURL=chunk-HX57TC2S.js.map
|
|
33
|
+
//# sourceMappingURL=chunk-HX57TC2S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useDynamicFetcher.ts"],"names":[],"mappings":";;;;AAsMO,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-HX57TC2S.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 *\n * function PostEditor({ postId }: { postId: string }) {\n * // Fetch post data\n * const fetcher = useDynamicFetcher<typeof import(\"./api.posts.$postId\")>(\n * \"/api/posts/:postId\",\n * { postId }\n * );\n *\n * // Submit updates\n * const submitter = useDynamicSubmitter<typeof import(\"./api.posts.$postId\")>(\n * \"/api/posts/:postId\",\n * { postId }\n * );\n *\n * useEffect(() => {\n * fetcher.load();\n * }, [fetcher.load]);\n *\n * const handleSave = async (title: string, content: string) => {\n * await submitter.submitJson({ title, content }, { method: \"PUT\" });\n * // Reload after save\n * fetcher.load();\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 * 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 disabled={submitter.state !== \"idle\"}>\n * {submitter.state === \"submitting\" ? \"Saving...\" : \"Save\"}\n * </button>\n * </form>\n * );\n * }\n * ```\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,55 @@
|
|
|
1
|
+
import { useMemo, useCallback } from 'react';
|
|
2
|
+
import { href, useFetcher } from 'react-router';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/useDynamicSubmitter.tsx
|
|
6
|
+
var useDynamicSubmitter = (path, ...args) => {
|
|
7
|
+
const url = useMemo(() => {
|
|
8
|
+
return href(path, ...args);
|
|
9
|
+
}, [path, args]);
|
|
10
|
+
const fetcher = useFetcher({
|
|
11
|
+
key: `submitter-${url}`
|
|
12
|
+
});
|
|
13
|
+
const submit = useCallback(
|
|
14
|
+
(target, options) => {
|
|
15
|
+
return fetcher.submit(target, {
|
|
16
|
+
...options,
|
|
17
|
+
method: options?.method ?? "POST",
|
|
18
|
+
action: url,
|
|
19
|
+
encType: "multipart/form-data"
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
[fetcher.submit, url]
|
|
23
|
+
);
|
|
24
|
+
const submitJson = useCallback(
|
|
25
|
+
(data, options = {}) => {
|
|
26
|
+
return fetcher.submit(
|
|
27
|
+
data,
|
|
28
|
+
{
|
|
29
|
+
...options,
|
|
30
|
+
method: options.method ?? "POST",
|
|
31
|
+
action: url,
|
|
32
|
+
encType: "application/json"
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
[fetcher.submit, url]
|
|
37
|
+
);
|
|
38
|
+
const OriginalForm = fetcher.Form;
|
|
39
|
+
const Form = useCallback(
|
|
40
|
+
({ method = "POST", ...props }) => {
|
|
41
|
+
return /* @__PURE__ */ jsx(OriginalForm, { action: url, method, ...props });
|
|
42
|
+
},
|
|
43
|
+
[url, OriginalForm]
|
|
44
|
+
);
|
|
45
|
+
return {
|
|
46
|
+
...fetcher,
|
|
47
|
+
submit,
|
|
48
|
+
submitJson,
|
|
49
|
+
Form
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { useDynamicSubmitter };
|
|
54
|
+
//# sourceMappingURL=chunk-JJN6GBJL.js.map
|
|
55
|
+
//# sourceMappingURL=chunk-JJN6GBJL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useDynamicSubmitter.tsx"],"names":[],"mappings":";;;;;AA6QO,IAAM,mBAAA,GAAsB,CAClC,IAAA,EAAA,GACG,IAAA,KAaC;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,aAAa,GAAG,CAAA;AAAA,GACrB,CAAA;AAED,EAAA,MAAM,MAAA,GAA4B,WAAA;AAAA,IACjC,CAAC,QAAQ,OAAA,KAAY;AACpB,MAAA,OAAO,OAAA,CAAQ,OAAO,MAAA,EAAQ;AAAA,QAC7B,GAAG,OAAA;AAAA,QACH,MAAA,EAAS,SAAS,MAAA,IACjB,MAAA;AAAA,QACD,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS;AAAA,OAC+B,CAAA;AAAA,IAC1C,CAAA;AAAA,IACA,CAAC,OAAA,CAAQ,MAAA,EAAQ,GAAG;AAAA,GACrB;AAEA,EAAA,MAAM,UAAA,GAAoC,WAAA;AAAA,IACzC,CAAC,IAAA,EAAM,OAAA,GAAU,EAAC,KAAM;AACvB,MAAA,OAAO,OAAA,CAAQ,MAAA;AAAA,QACd,IAAA;AAAA,QACA;AAAA,UACC,GAAG,OAAA;AAAA,UACH,MAAA,EAAS,QAAQ,MAAA,IAChB,MAAA;AAAA,UACD,MAAA,EAAQ,GAAA;AAAA,UACR,OAAA,EAAS;AAAA;AACV,OACD;AAAA,IACD,CAAA;AAAA,IACA,CAAC,OAAA,CAAQ,MAAA,EAAQ,GAAG;AAAA,GACrB;AAEA,EAAA,MAAM,eAAe,OAAA,CAAQ,IAAA;AAE7B,EAAA,MAAM,IAAA,GAAmB,WAAA;AAAA,IACxB,CAAC,EAAE,MAAA,GAAS,MAAA,EAAQ,GAAG,OAAM,KAAM;AAClC,MAAA,2BAAQ,YAAA,EAAA,EAAa,MAAA,EAAQ,GAAA,EAAK,MAAA,EAAiB,GAAG,KAAA,EAAO,CAAA;AAAA,IAC9D,CAAA;AAAA,IACA,CAAC,KAAK,YAAY;AAAA,GACnB;AAEA,EAAA,OAAO;AAAA,IACN,GAAG,OAAA;AAAA,IACH,MAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACD;AACD","file":"chunk-JJN6GBJL.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 * @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 * // submitter.data is the typed response from the action\n * // submitter.state is \"idle\" | \"loading\" | \"submitting\"\n *\n * // Option 1: Submit as JSON (recommended for programmatic submissions)\n * // Defaults to POST if no options provided\n * const handleSubmitJson = async () => {\n * await submitter.submitJson({\n * title: \"My Post\",\n * content: \"Post content here\",\n * published: true,\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)\n * return (\n * <submitter.Form>\n * <input name=\"title\" />\n * <textarea name=\"content\" />\n * <button type=\"submit\">Save</button>\n * </submitter.Form>\n * );\n * }\n * ```\n *\n * @example\n * ### Handling responses\n *\n * ```tsx\n * function LoginForm() {\n * const submitter = useDynamicSubmitter<typeof import(\"./auth.login\")>(\"/auth/login\");\n *\n * useEffect(() => {\n * if (submitter.data?.success) {\n * // Handle success\n * console.log(\"Logged in as:\", submitter.data.value.user.email);\n * } else if (submitter.data && !submitter.data.success) {\n * // Handle error\n * if (submitter.data.error.type === \"validation\") {\n * console.log(\"Validation errors:\", submitter.data.error.error);\n * }\n * }\n * }, [submitter.data]);\n *\n * return (\n * <submitter.Form>\n * <input name=\"email\" type=\"email\" />\n * <input name=\"password\" type=\"password\" />\n * <button disabled={submitter.state !== \"idle\"}>\n * {submitter.state === \"submitting\" ? \"Logging in...\" : \"Login\"}\n * </button>\n * </submitter.Form>\n * );\n * }\n * ```\n */\n\n// biome-ignore lint/style/useImportType: We need to import React here.\nimport React, { useCallback, useMemo } from \"react\";\nimport {\n\ttype FetcherFormProps,\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 * 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<void>;\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<void>;\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 * 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 args - Route parameters (if the route has dynamic segments like `:id`)\n *\n * @returns An extended fetcher object with:\n * - `submit` - Submit with FormData/SubmitTarget (includes schema type)\n * - `submitJson` - Submit a plain object as JSON (schema type only)\n * - `Form` - Pre-bound form component\n * - `data` - Response data from the action (typed)\n * - `state` - Fetcher state (\"idle\" | \"loading\" | \"submitting\")\n * - All other useFetcher properties\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 * // Submit using submitJson (type-safe, no FormData needed, defaults to POST)\n * await submitter.submitJson({\n * displayName: \"John Doe\",\n * email: \"john@example.com\",\n * notifications: true,\n * });\n *\n * // Check the response\n * if (submitter.data?.success) {\n * console.log(\"Settings updated!\");\n * }\n * ```\n */\nexport const useDynamicSubmitter = <TInfo extends RouteWithActionModule>(\n\tpath: TInfo[\"route\"],\n\t...args: TInfo[\"route\"] extends \"undefined\"\n\t\t? HrefArgs<\"/\">\n\t\t: HrefArgs<TInfo[\"route\"]>\n): Omit<\n\tReturnType<typeof useFetcher<TInfo[\"action\"]>>,\n\t\"load\" | \"submit\" | \"Form\"\n> & {\n\t/** Submit with FormData or SubmitTarget (schema type & SubmitTarget) */\n\tsubmit: SubmitFunc<TInfo>;\n\t/** Submit a plain object as JSON (schema type only, defaults to POST) */\n\tsubmitJson: SubmitJsonFunc<TInfo>;\n\t/** Pre-bound Form component with action URL already set (defaults to POST) */\n\tForm: SubmitForm;\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[\"action\"]>({\n\t\tkey: `submitter-${url}`,\n\t});\n\n\tconst submit: SubmitFunc<TInfo> = useCallback(\n\t\t(target, options) => {\n\t\t\treturn fetcher.submit(target, {\n\t\t\t\t...options,\n\t\t\t\tmethod: (options?.method ??\n\t\t\t\t\t\"POST\") as import(\"react-router\").HTMLFormMethod,\n\t\t\t\taction: url,\n\t\t\t\tencType: \"multipart/form-data\",\n\t\t\t} as Parameters<typeof fetcher.submit>[1]);\n\t\t},\n\t\t[fetcher.submit, url],\n\t);\n\n\tconst submitJson: SubmitJsonFunc<TInfo> = useCallback(\n\t\t(data, options = {}) => {\n\t\t\treturn fetcher.submit(\n\t\t\t\tdata as SubmitTarget,\n\t\t\t\t{\n\t\t\t\t\t...options,\n\t\t\t\t\tmethod: (options.method ??\n\t\t\t\t\t\t\"POST\") as import(\"react-router\").HTMLFormMethod,\n\t\t\t\t\taction: url,\n\t\t\t\t\tencType: \"application/json\",\n\t\t\t\t} as Parameters<typeof fetcher.submit>[1],\n\t\t\t);\n\t\t},\n\t\t[fetcher.submit, url],\n\t);\n\n\tconst OriginalForm = fetcher.Form;\n\n\tconst Form: SubmitForm = useCallback(\n\t\t({ method = \"POST\", ...props }) => {\n\t\t\treturn <OriginalForm action={url} method={method} {...props} />;\n\t\t},\n\t\t[url, OriginalForm],\n\t);\n\n\treturn {\n\t\t...fetcher,\n\t\tsubmit,\n\t\tsubmitJson,\n\t\tForm,\n\t};\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-MKH4ZP5U.js"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
2
|
+
import { href } from 'react-router';
|
|
3
|
+
|
|
4
|
+
// src/useCachedFetch.ts
|
|
5
|
+
var fetchCache = /* @__PURE__ */ new Map();
|
|
6
|
+
var useCachedFetch = (path, ...args) => {
|
|
7
|
+
const url = useMemo(() => {
|
|
8
|
+
return href(path, ...args);
|
|
9
|
+
}, [path, args]);
|
|
10
|
+
const cacheKey = url;
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState(void 0);
|
|
13
|
+
const [data, setData] = useState(
|
|
14
|
+
() => fetchCache.has(cacheKey) ? fetchCache.get(cacheKey) : void 0
|
|
15
|
+
);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const fetchData = async () => {
|
|
18
|
+
if (fetchCache.has(cacheKey)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setIsLoading(true);
|
|
22
|
+
setError(void 0);
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
const result = await response.json();
|
|
29
|
+
fetchCache.set(cacheKey, result);
|
|
30
|
+
setData(result);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
33
|
+
} finally {
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
fetchData();
|
|
38
|
+
}, [url, cacheKey]);
|
|
39
|
+
return { data, isLoading, error };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { useCachedFetch };
|
|
43
|
+
//# sourceMappingURL=chunk-OQGTU34H.js.map
|
|
44
|
+
//# sourceMappingURL=chunk-OQGTU34H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useCachedFetch.ts"],"names":[],"mappings":";;;;AAMA,IAAM,UAAA,uBAAiB,GAAA,EAAqB;AAGrC,IAAM,cAAA,GAAiB,CAC7B,IAAA,EAAA,GACG,IAAA,KAOC;AAEJ,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;AAGf,EAAA,MAAM,QAAA,GAAW,GAAA;AAGjB,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,KAAK,CAAA;AAChD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAA4B,MAAS,CAAA;AAC/D,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,QAAA;AAAA,IAEtB,MACD,WAAW,GAAA,CAAI,QAAQ,IACnB,UAAA,CAAW,GAAA,CAAI,QAAQ,CAAA,GAGxB;AAAA,GACJ;AAGA,EAAA,SAAA,CAAU,MAAM;AACf,IAAA,MAAM,YAAY,YAAY;AAE7B,MAAA,IAAI,UAAA,CAAW,GAAA,CAAI,QAAQ,CAAA,EAAG;AAC7B,QAAA;AAAA,MACD;AAEA,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,QAAA,CAAS,MAAS,CAAA;AAElB,MAAA,IAAI;AACH,QAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,QAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACjB,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,QACzD;AAEA,QAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,IAAA,EAAK;AAGnC,QAAA,UAAA,CAAW,GAAA,CAAI,UAAU,MAAM,CAAA;AAC/B,QAAA,OAAA,CAAQ,MAA2D,CAAA;AAAA,MACpE,SAAS,GAAA,EAAK;AACb,QAAA,QAAA,CAAS,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAAA,MAC7D,CAAA,SAAE;AACD,QAAA,YAAA,CAAa,KAAK,CAAA;AAAA,MACnB;AAAA,IACD,CAAA;AAEA,IAAA,SAAA,EAAU;AAAA,EACX,CAAA,EAAG,CAAC,GAAA,EAAK,QAAQ,CAAC,CAAA;AAElB,EAAA,OAAO,EAAE,IAAA,EAAM,SAAA,EAAW,KAAA,EAAM;AACjC","file":"chunk-OQGTU34H.js","sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { href, type useLoaderData } from \"react-router\";\nimport type { HrefArgs } from \"./types/HrefArgs\";\nimport type { RouteWithLoaderModule } from \"./types/RouteWithLoaderModule\";\n\n// Cache for the useCachedFetch hook (regular fetch, not useFetcher)\nconst fetchCache = new Map<string, unknown>();\n\n// Hook that uses regular fetch instead of useFetcher to avoid route invalidation\nexport const useCachedFetch = <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): {\n\tdata: ReturnType<typeof useLoaderData<TInfo[\"loader\"]>> | undefined;\n\tisLoading: boolean;\n\terror: Error | undefined;\n} => {\n\t// Generate URL using href, same as useDynamicFetcher\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\t// Use the generated URL as the cache key\n\tconst cacheKey = url;\n\n\t// Local state\n\tconst [isLoading, setIsLoading] = useState(false);\n\tconst [error, setError] = useState<Error | undefined>(undefined);\n\tconst [data, setData] = useState<\n\t\tReturnType<typeof useLoaderData<TInfo[\"loader\"]>> | undefined\n\t>(() =>\n\t\tfetchCache.has(cacheKey)\n\t\t\t? (fetchCache.get(cacheKey) as ReturnType<\n\t\t\t\t\ttypeof useLoaderData<TInfo[\"loader\"]>\n\t\t\t\t>)\n\t\t\t: undefined,\n\t);\n\n\t// Auto-fetch on mount or when URL changes, if not in cache\n\tuseEffect(() => {\n\t\tconst fetchData = async () => {\n\t\t\t// Skip fetch if data is already cached\n\t\t\tif (fetchCache.has(cacheKey)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetIsLoading(true);\n\t\t\tsetError(undefined);\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(url);\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`HTTP error! Status: ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tconst result = await response.json();\n\n\t\t\t\t// Update cache and state\n\t\t\t\tfetchCache.set(cacheKey, result);\n\t\t\t\tsetData(result as ReturnType<typeof useLoaderData<TInfo[\"loader\"]>>);\n\t\t\t} catch (err) {\n\t\t\t\tsetError(err instanceof Error ? err : new Error(String(err)));\n\t\t\t} finally {\n\t\t\t\tsetIsLoading(false);\n\t\t\t}\n\t\t};\n\n\t\tfetchData();\n\t}, [url, cacheKey]);\n\n\treturn { data, isLoading, error };\n};\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ConcurrentSubmitterContext } from './chunk-ZC6U3MIB.js';
|
|
2
|
+
import { useContext, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
function useConcurrentSubmitter() {
|
|
5
|
+
const ctx = useContext(ConcurrentSubmitterContext);
|
|
6
|
+
if (!ctx) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"useConcurrentSubmitter must be used within a ConcurrentSubmitterProvider"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
function isRouteParams(obj) {
|
|
12
|
+
return typeof obj === "object" && obj !== null && !Array.isArray(obj) && obj instanceof FormData === false && Object.values(obj).every(
|
|
13
|
+
(v) => typeof v === "string"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const submitJson = useCallback(
|
|
17
|
+
(path, ...rest) => {
|
|
18
|
+
if (rest.length === 1) {
|
|
19
|
+
return ctx.addJsonSubmission(path, void 0, rest[0], void 0);
|
|
20
|
+
}
|
|
21
|
+
if (rest.length === 2 && rest[0] === void 0) {
|
|
22
|
+
return ctx.addJsonSubmission(path, void 0, rest[1], void 0);
|
|
23
|
+
}
|
|
24
|
+
if (rest.length === 2 && !isRouteParams(rest[0])) {
|
|
25
|
+
const [data2, options2] = rest;
|
|
26
|
+
return ctx.addJsonSubmission(path, void 0, data2, options2);
|
|
27
|
+
}
|
|
28
|
+
const [args, data, options] = rest;
|
|
29
|
+
return ctx.addJsonSubmission(path, args, data, options);
|
|
30
|
+
},
|
|
31
|
+
[ctx]
|
|
32
|
+
);
|
|
33
|
+
const submitFormData = useCallback(
|
|
34
|
+
(path, ...rest) => {
|
|
35
|
+
const second = rest[0];
|
|
36
|
+
if (second instanceof FormData) {
|
|
37
|
+
const formData2 = second;
|
|
38
|
+
const submittedData2 = rest.length >= 2 ? rest[1] : {};
|
|
39
|
+
const options2 = rest.length === 3 ? rest[2] : void 0;
|
|
40
|
+
return ctx.addFormSubmission(
|
|
41
|
+
path,
|
|
42
|
+
void 0,
|
|
43
|
+
formData2,
|
|
44
|
+
submittedData2,
|
|
45
|
+
options2
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const args = second;
|
|
49
|
+
const formData = rest[1];
|
|
50
|
+
const submittedData = rest.length >= 3 ? rest[2] : {};
|
|
51
|
+
const options = rest.length === 4 ? rest[3] : void 0;
|
|
52
|
+
return ctx.addFormSubmission(
|
|
53
|
+
path,
|
|
54
|
+
args,
|
|
55
|
+
formData,
|
|
56
|
+
submittedData,
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
[ctx]
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
operations: ctx.operations,
|
|
64
|
+
submitJson,
|
|
65
|
+
submitFormData
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { useConcurrentSubmitter };
|
|
70
|
+
//# sourceMappingURL=chunk-QZDEBX3D.js.map
|
|
71
|
+
//# sourceMappingURL=chunk-QZDEBX3D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useConcurrentSubmitter.tsx"],"names":["data","options","formData","submittedData"],"mappings":";;;AAuGO,SAAS,sBAAA,GAEyB;AACxC,EAAA,MAAM,GAAA,GAAM,WAAW,0BAA0B,CAAA;AACjD,EAAA,IAAI,CAAC,GAAA,EAAK;AACT,IAAA,MAAM,IAAI,KAAA;AAAA,MACT;AAAA,KACD;AAAA,EACD;AAEA,EAAA,SAAS,cAAc,GAAA,EAA6C;AACnE,IAAA,OACC,OAAO,GAAA,KAAQ,QAAA,IACf,GAAA,KAAQ,IAAA,IACR,CAAC,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,IAClB,eAAe,QAAA,KAAa,KAAA,IAC5B,MAAA,CAAO,MAAA,CAAO,GAA8B,CAAA,CAAE,KAAA;AAAA,MAC7C,CAAC,CAAA,KAAM,OAAO,CAAA,KAAM;AAAA,KACrB;AAAA,EAEF;AAEA,EAAA,MAAM,UAAA,GAAa,WAAA;AAAA,IAClB,CAAC,SAAiB,IAAA,KAAoB;AACrC,MAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACtB,QAAA,OAAO,IAAI,iBAAA,CAAkB,IAAA,EAAM,QAAW,IAAA,CAAK,CAAC,GAAG,MAAS,CAAA;AAAA,MACjE;AACA,MAAA,IAAI,KAAK,MAAA,KAAW,CAAA,IAAK,IAAA,CAAK,CAAC,MAAM,MAAA,EAAW;AAC/C,QAAA,OAAO,IAAI,iBAAA,CAAkB,IAAA,EAAM,QAAW,IAAA,CAAK,CAAC,GAAG,MAAS,CAAA;AAAA,MACjE;AACA,MAAA,IAAI,IAAA,CAAK,WAAW,CAAA,IAAK,CAAC,cAAc,IAAA,CAAK,CAAC,CAAC,CAAA,EAAG;AACjD,QAAA,MAAM,CAACA,KAAAA,EAAMC,QAAO,CAAA,GAAI,IAAA;AAIxB,QAAA,OAAO,GAAA,CAAI,iBAAA,CAAkB,IAAA,EAAM,MAAA,EAAWD,OAAMC,QAAO,CAAA;AAAA,MAC5D;AACA,MAAA,MAAM,CAAC,IAAA,EAAM,IAAA,EAAM,OAAO,CAAA,GAAI,IAAA;AAK9B,MAAA,OAAO,GAAA,CAAI,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,MAAM,OAAO,CAAA;AAAA,IACvD,CAAA;AAAA,IACA,CAAC,GAAG;AAAA,GACL;AAEA,EAAA,MAAM,cAAA,GAAiB,WAAA;AAAA,IACtB,CAAC,SAAiB,IAAA,KAAoB;AACrC,MAAA,MAAM,MAAA,GAAS,KAAK,CAAC,CAAA;AACrB,MAAA,IAAI,kBAAkB,QAAA,EAAU;AAC/B,QAAA,MAAMC,SAAAA,GAAW,MAAA;AACjB,QAAA,MAAMC,iBACL,IAAA,CAAK,MAAA,IAAU,IAAK,IAAA,CAAK,CAAC,IAA8B,EAAC;AAC1D,QAAA,MAAMF,WACL,IAAA,CAAK,MAAA,KAAW,CAAA,GAAK,IAAA,CAAK,CAAC,CAAA,GAA8B,MAAA;AAC1D,QAAA,OAAO,GAAA,CAAI,iBAAA;AAAA,UACV,IAAA;AAAA,UACA,MAAA;AAAA,UACAC,SAAAA;AAAA,UACAC,cAAAA;AAAA,UACAF;AAAA,SACD;AAAA,MACD;AACA,MAAA,MAAM,IAAA,GAAO,MAAA;AACb,MAAA,MAAM,QAAA,GAAW,KAAK,CAAC,CAAA;AACvB,MAAA,MAAM,gBACL,IAAA,CAAK,MAAA,IAAU,IAAK,IAAA,CAAK,CAAC,IAA8B,EAAC;AAC1D,MAAA,MAAM,UAAW,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,IAAA,CAAK,CAAC,CAAA,GAAI,MAAA;AAG/C,MAAA,OAAO,GAAA,CAAI,iBAAA;AAAA,QACV,IAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,aAAA;AAAA,QACA;AAAA,OACD;AAAA,IACD,CAAA;AAAA,IACA,CAAC,GAAG;AAAA,GACL;AAEA,EAAA,OAAO;AAAA,IACN,YACC,GAAA,CAAI,UAAA;AAAA,IACL,UAAA;AAAA,IACA;AAAA,GACD;AACD","file":"chunk-QZDEBX3D.js","sourcesContent":["/**\n * @fileoverview Typed hook for concurrent form submissions. Use within ConcurrentSubmitterProvider.\n */\n\nimport { useCallback, useContext } from \"react\";\nimport type { z } from \"zod\";\nimport type { RegisterPages } from \"./types/RegisterPages\";\nimport type {\n\tActionResult,\n\tRouteWithActionModule,\n} from \"./types/RouteWithActionModule\";\nimport { ConcurrentSubmitterContext } from \"./ConcurrentSubmitterProvider\";\nimport type {\n\tFormDataSubmittedData,\n\tOperation,\n\tSubmitFormDataOptions,\n\tSubmitJsonOptions,\n\tSubmitJsonResult,\n} from \"./ConcurrentSubmitterProvider\";\n\nexport type { ActionResult };\n\ntype RouteParams<R extends keyof RegisterPages> = RegisterPages[R][\"params\"];\n\n/**\n * True when the route has no dynamic segments (`params: {}` from React Router typegen).\n * Uses `keyof … extends never` so real empty params stay distinct from the `RegisterPages`\n * fallback (`AnyPages` → `params: Record<string, …>` has `keyof` = `string`, not `never`).\n * That avoids mis-resolving `submitJson(path, data)` as the route-args overload (strings only).\n */\ntype HasNoParams<R extends keyof RegisterPages> = [\n\tkeyof RegisterPages[R][\"params\"],\n] extends [never]\n\t? true\n\t: false;\n\ntype HasOptionalParams<R extends keyof RegisterPages> =\n\tHasNoParams<R> extends true\n\t\t? false\n\t\t: Partial<RouteParams<R>> extends RouteParams<R>\n\t\t\t? true\n\t\t\t: false;\n\n// Args immediately after path (when needed), matching useDynamicFetcher/useDynamicSubmitter.\n// Order: (path, args?, data, options?) and (path, args?, formData, submittedData?, options?).\ntype SubmitJsonFn<TInfo extends RouteWithActionModule> =\n\tHasNoParams<TInfo[\"route\"]> extends true\n\t\t? (\n\t\t\t\tpath: TInfo[\"route\"],\n\t\t\t\tdata: z.infer<TInfo[\"formSchema\"]>,\n\t\t\t\toptions?: SubmitJsonOptions,\n\t\t\t) => SubmitJsonResult<ActionResult<TInfo>>\n\t\t: HasOptionalParams<TInfo[\"route\"]> extends true\n\t\t\t? (\n\t\t\t\t\tpath: TInfo[\"route\"],\n\t\t\t\t\targs: RouteParams<TInfo[\"route\"]> | undefined,\n\t\t\t\t\tdata: z.infer<TInfo[\"formSchema\"]>,\n\t\t\t\t\toptions?: SubmitJsonOptions,\n\t\t\t\t) => SubmitJsonResult<ActionResult<TInfo>>\n\t\t\t: (\n\t\t\t\t\tpath: TInfo[\"route\"],\n\t\t\t\t\targs: RouteParams<TInfo[\"route\"]>,\n\t\t\t\t\tdata: z.infer<TInfo[\"formSchema\"]>,\n\t\t\t\t\toptions?: SubmitJsonOptions,\n\t\t\t\t) => SubmitJsonResult<ActionResult<TInfo>>;\n\ntype SubmitFormDataFn<TInfo extends RouteWithActionModule> =\n\tHasNoParams<TInfo[\"route\"]> extends true\n\t\t? (\n\t\t\t\tpath: TInfo[\"route\"],\n\t\t\t\tformData: FormData,\n\t\t\t\tsubmittedData?: FormDataSubmittedData,\n\t\t\t\toptions?: SubmitFormDataOptions,\n\t\t\t) => SubmitJsonResult<ActionResult<TInfo>>\n\t\t: HasOptionalParams<TInfo[\"route\"]> extends true\n\t\t\t? (\n\t\t\t\t\tpath: TInfo[\"route\"],\n\t\t\t\t\targs: RouteParams<TInfo[\"route\"]> | undefined,\n\t\t\t\t\tformData: FormData,\n\t\t\t\t\tsubmittedData?: FormDataSubmittedData,\n\t\t\t\t\toptions?: SubmitFormDataOptions,\n\t\t\t\t) => SubmitJsonResult<ActionResult<TInfo>>\n\t\t\t: (\n\t\t\t\t\tpath: TInfo[\"route\"],\n\t\t\t\t\targs: RouteParams<TInfo[\"route\"]>,\n\t\t\t\t\tformData: FormData,\n\t\t\t\t\tsubmittedData?: FormDataSubmittedData,\n\t\t\t\t\toptions?: SubmitFormDataOptions,\n\t\t\t\t) => SubmitJsonResult<ActionResult<TInfo>>;\n\nexport type UseConcurrentSubmitterReturn<TInfo extends RouteWithActionModule> =\n\t{\n\t\toperations: Record<\n\t\t\tstring,\n\t\t\tOperation<\n\t\t\t\tActionResult<TInfo>,\n\t\t\t\tz.infer<TInfo[\"formSchema\"]> | FormDataSubmittedData\n\t\t\t>\n\t\t>;\n\t\tsubmitJson: SubmitJsonFn<TInfo>;\n\t\tsubmitFormData: SubmitFormDataFn<TInfo>;\n\t};\n\nexport function useConcurrentSubmitter<\n\tTInfo extends RouteWithActionModule,\n>(): UseConcurrentSubmitterReturn<TInfo> {\n\tconst ctx = useContext(ConcurrentSubmitterContext);\n\tif (!ctx) {\n\t\tthrow new Error(\n\t\t\t\"useConcurrentSubmitter must be used within a ConcurrentSubmitterProvider\",\n\t\t);\n\t}\n\n\tfunction isRouteParams(obj: unknown): obj is Record<string, string> {\n\t\treturn (\n\t\t\ttypeof obj === \"object\" &&\n\t\t\tobj !== null &&\n\t\t\t!Array.isArray(obj) &&\n\t\t\tobj instanceof FormData === false &&\n\t\t\tObject.values(obj as Record<string, unknown>).every(\n\t\t\t\t(v) => typeof v === \"string\",\n\t\t\t)\n\t\t);\n\t}\n\n\tconst submitJson = useCallback(\n\t\t(path: string, ...rest: unknown[]) => {\n\t\t\tif (rest.length === 1) {\n\t\t\t\treturn ctx.addJsonSubmission(path, undefined, rest[0], undefined);\n\t\t\t}\n\t\t\tif (rest.length === 2 && rest[0] === undefined) {\n\t\t\t\treturn ctx.addJsonSubmission(path, undefined, rest[1], undefined);\n\t\t\t}\n\t\t\tif (rest.length === 2 && !isRouteParams(rest[0])) {\n\t\t\t\tconst [data, options] = rest as [\n\t\t\t\t\tRecord<string, unknown>,\n\t\t\t\t\tSubmitJsonOptions | undefined,\n\t\t\t\t];\n\t\t\t\treturn ctx.addJsonSubmission(path, undefined, data, options);\n\t\t\t}\n\t\t\tconst [args, data, options] = rest as [\n\t\t\t\tRecord<string, string> | undefined,\n\t\t\t\tRecord<string, unknown>,\n\t\t\t\tSubmitJsonOptions | undefined,\n\t\t\t];\n\t\t\treturn ctx.addJsonSubmission(path, args, data, options);\n\t\t},\n\t\t[ctx],\n\t) as UseConcurrentSubmitterReturn<TInfo>[\"submitJson\"];\n\n\tconst submitFormData = useCallback(\n\t\t(path: string, ...rest: unknown[]) => {\n\t\t\tconst second = rest[0];\n\t\t\tif (second instanceof FormData) {\n\t\t\t\tconst formData = second;\n\t\t\t\tconst submittedData =\n\t\t\t\t\trest.length >= 2 ? (rest[1] as FormDataSubmittedData) : {};\n\t\t\t\tconst options =\n\t\t\t\t\trest.length === 3 ? (rest[2] as SubmitFormDataOptions) : undefined;\n\t\t\t\treturn ctx.addFormSubmission(\n\t\t\t\t\tpath,\n\t\t\t\t\tundefined,\n\t\t\t\t\tformData,\n\t\t\t\t\tsubmittedData,\n\t\t\t\t\toptions,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst args = second as Record<string, string>;\n\t\t\tconst formData = rest[1] as FormData;\n\t\t\tconst submittedData =\n\t\t\t\trest.length >= 3 ? (rest[2] as FormDataSubmittedData) : {};\n\t\t\tconst options = (rest.length === 4 ? rest[3] : undefined) as\n\t\t\t\t| SubmitFormDataOptions\n\t\t\t\t| undefined;\n\t\t\treturn ctx.addFormSubmission(\n\t\t\t\tpath,\n\t\t\t\targs,\n\t\t\t\tformData,\n\t\t\t\tsubmittedData,\n\t\t\t\toptions,\n\t\t\t);\n\t\t},\n\t\t[ctx],\n\t) as UseConcurrentSubmitterReturn<TInfo>[\"submitFormData\"];\n\n\treturn {\n\t\toperations:\n\t\t\tctx.operations as UseConcurrentSubmitterReturn<TInfo>[\"operations\"],\n\t\tsubmitJson,\n\t\tsubmitFormData,\n\t};\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-R6HEJ5E5.js"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-S4QCJFVR.js"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/useFetcherStateChanged.ts
|
|
4
|
+
var useFetcherStateChanged = (fetcher, onChange) => {
|
|
5
|
+
const lastStateRef = useRef(fetcher.state);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (lastStateRef.current !== fetcher.state) {
|
|
8
|
+
onChange(lastStateRef.current, fetcher.state);
|
|
9
|
+
lastStateRef.current = fetcher.state;
|
|
10
|
+
}
|
|
11
|
+
}, [fetcher.state, onChange]);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { useFetcherStateChanged };
|
|
15
|
+
//# sourceMappingURL=chunk-W5R7YYHR.js.map
|
|
16
|
+
//# sourceMappingURL=chunk-W5R7YYHR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useFetcherStateChanged.ts"],"names":[],"mappings":";;;AAQO,IAAM,sBAAA,GAAyB,CACrC,OAAA,EACA,QAAA,KAII;AACJ,EAAA,MAAM,YAAA,GAAe,MAAA,CAA6B,OAAA,CAAQ,KAAK,CAAA;AAE/D,EAAA,SAAA,CAAU,MAAM;AACf,IAAA,IAAI,YAAA,CAAa,OAAA,KAAY,OAAA,CAAQ,KAAA,EAAO;AAC3C,MAAA,QAAA,CAAS,YAAA,CAAa,OAAA,EAAS,OAAA,CAAQ,KAAK,CAAA;AAC5C,MAAA,YAAA,CAAa,UAAU,OAAA,CAAQ,KAAA;AAAA,IAChC;AAAA,EACD,CAAA,EAAG,CAAC,OAAA,CAAQ,KAAA,EAAO,QAAQ,CAAC,CAAA;AAC7B","file":"chunk-W5R7YYHR.js","sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { useFetcher } from \"react-router\";\n\n/**\n * A hook that tracks changes in a fetcher's state and calls a callback when it changes.\n * @param fetcher The fetcher instance to track\n * @param onChange Callback that receives the previous state and new state when the state changes\n */\nexport const useFetcherStateChanged = (\n\tfetcher: Pick<ReturnType<typeof useFetcher>, \"state\">,\n\tonChange: (\n\t\tlastState: typeof fetcher.state | undefined,\n\t\tnewState: typeof fetcher.state,\n\t) => void,\n) => {\n\tconst lastStateRef = useRef<typeof fetcher.state>(fetcher.state);\n\n\tuseEffect(() => {\n\t\tif (lastStateRef.current !== fetcher.state) {\n\t\t\tonChange(lastStateRef.current, fetcher.state);\n\t\t\tlastStateRef.current = fetcher.state;\n\t\t}\n\t}, [fetcher.state, onChange]);\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-YYJDSKJG.js"}
|