@firtoz/router-toolkit 1.3.0 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/router-toolkit",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Type-safe React Router 7 framework mode helpers with enhanced fetching, form submission, and state management",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -59,5 +59,8 @@
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public"
62
+ },
63
+ "dependencies": {
64
+ "zod-form-data": "^3.0.0"
62
65
  }
63
66
  }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @fileoverview Type-safe form action utility for React Router 7
3
+ *
4
+ * This module provides a wrapper for React Router actions that handles form data validation
5
+ * using Zod schemas and provides structured error handling with MaybeError.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { z } from "zod/v4";
10
+ * import { formAction } from "@firtoz/router-toolkit";
11
+ * import { success } from "@firtoz/maybe-error";
12
+ *
13
+ * const schema = z.object({
14
+ * email: z.string().email(),
15
+ * password: z.string().min(8),
16
+ * });
17
+ *
18
+ * export const action = formAction({
19
+ * schema,
20
+ * handler: async (args, data) => {
21
+ * // data is fully typed based on the schema
22
+ * const user = await authenticateUser(data.email, data.password);
23
+ * return success(user);
24
+ * },
25
+ * });
26
+ * ```
27
+ */
28
+
29
+ import { fail, type MaybeError } from "@firtoz/maybe-error";
30
+ import type { ActionFunctionArgs } from "react-router";
31
+ import { z } from "zod/v4";
32
+ import { zfd } from "zod-form-data";
33
+
34
+ /**
35
+ * Error types that can be returned by formAction
36
+ */
37
+ export type FormActionError<TError> =
38
+ | {
39
+ type: "validation";
40
+ error: ReturnType<typeof z.treeifyError<z.ZodTypeAny>>;
41
+ }
42
+ | {
43
+ type: "handler";
44
+ error: TError;
45
+ }
46
+ | {
47
+ type: "unknown";
48
+ };
49
+
50
+ /**
51
+ * Configuration object for formAction
52
+ *
53
+ * @template TSchema - The Zod schema type for form validation
54
+ * @template TResult - The success result type from the handler
55
+ * @template TError - The error type that the handler can return
56
+ * @template ActionArgs - The action function arguments type (defaults to ActionFunctionArgs)
57
+ */
58
+ export interface FormActionConfig<
59
+ TSchema extends z.ZodTypeAny,
60
+ TResult = undefined,
61
+ TError = string,
62
+ ActionArgs extends ActionFunctionArgs = ActionFunctionArgs,
63
+ > {
64
+ /**
65
+ * Zod schema to validate the form data against
66
+ */
67
+ schema: TSchema;
68
+ /**
69
+ * Handler function that processes the validated form data
70
+ *
71
+ * @param args - The original action function arguments
72
+ * @param data - The validated form data (typed according to the schema)
73
+ * @returns A promise that resolves to a MaybeError with the result or error
74
+ */
75
+ handler: (
76
+ args: ActionArgs,
77
+ data: z.infer<TSchema>,
78
+ ) => Promise<MaybeError<TResult, TError>>;
79
+ }
80
+
81
+ /**
82
+ * Creates a type-safe form action handler that validates form data and provides structured error handling.
83
+ *
84
+ * This function wraps a React Router action to:
85
+ * 1. Parse and validate form data using a Zod schema
86
+ * 2. Call the provided handler with validated data
87
+ * 3. Return structured errors for validation failures, handler errors, or unknown errors
88
+ * 4. Preserve React Router Response objects (redirects, etc.) by re-throwing them
89
+ *
90
+ * @template TSchema - The Zod schema type for form validation
91
+ * @template TResult - The success result type from the handler (defaults to undefined)
92
+ * @template TError - The error type that the handler can return (defaults to string)
93
+ * @template ActionArgs - The action function arguments type (defaults to ActionFunctionArgs)
94
+ *
95
+ * @param config - Configuration object containing schema and handler
96
+ * @returns An action function that can be used with React Router
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import { z } from "zod/v4";
101
+ * import { formAction } from "@firtoz/router-toolkit";
102
+ * import { success, fail } from "@firtoz/maybe-error";
103
+ *
104
+ * const loginSchema = z.object({
105
+ * email: z.string().email("Invalid email format"),
106
+ * password: z.string().min(8, "Password must be at least 8 characters"),
107
+ * });
108
+ *
109
+ * export const action = formAction({
110
+ * schema: loginSchema,
111
+ * handler: async (args, data) => {
112
+ * try {
113
+ * const user = await authenticateUser(data.email, data.password);
114
+ * return success(user);
115
+ * } catch (error) {
116
+ * return fail("Invalid credentials");
117
+ * }
118
+ * },
119
+ * });
120
+ * ```
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * // In your component, handle the different error types:
125
+ * const actionData = useActionData<typeof action>();
126
+ *
127
+ * if (actionData && !actionData.success) {
128
+ * switch (actionData.error.type) {
129
+ * case "validation":
130
+ * // Handle validation errors - actionData.error.error contains field-specific errors
131
+ * break;
132
+ * case "handler":
133
+ * // Handle business logic errors - actionData.error.error contains your custom error
134
+ * break;
135
+ * case "unknown":
136
+ * // Handle unexpected errors
137
+ * break;
138
+ * }
139
+ * }
140
+ * ```
141
+ */
142
+ export const formAction = <
143
+ TSchema extends z.ZodTypeAny,
144
+ TResult = undefined,
145
+ TError = string,
146
+ ActionArgs extends ActionFunctionArgs = ActionFunctionArgs,
147
+ >({
148
+ schema,
149
+ handler,
150
+ }: FormActionConfig<TSchema, TResult, TError, ActionArgs>) => {
151
+ return async (
152
+ args: ActionArgs,
153
+ ): Promise<MaybeError<TResult, FormActionError<TError>>> => {
154
+ try {
155
+ const rawFormData = await args.request.formData();
156
+ const formData = await zfd.formData(schema).safeParseAsync(rawFormData);
157
+
158
+ if (!formData.success) {
159
+ return fail({
160
+ type: "validation" as const,
161
+ error: z.treeifyError<TSchema>(
162
+ formData.error as z.core.$ZodError<TSchema>,
163
+ ),
164
+ });
165
+ }
166
+
167
+ const handlerResult = await handler(args, formData.data);
168
+ if (!handlerResult.success) {
169
+ return fail({
170
+ type: "handler" as const,
171
+ error: handlerResult.error,
172
+ });
173
+ }
174
+
175
+ return handlerResult;
176
+ } catch (error) {
177
+ // Re-throw Response objects (redirects, etc.) to preserve React Router behavior
178
+ if (error instanceof Response) {
179
+ throw error;
180
+ }
181
+
182
+ console.error("Unexpected error in formAction:", error);
183
+ return fail({
184
+ type: "unknown" as const,
185
+ });
186
+ }
187
+ };
188
+ };
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./formAction";
1
2
  export * from "./types/index";
2
3
  export * from "./useCachedFetch";
3
4
  export * from "./useDynamicFetcher";