react-form-action 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,539 +1,543 @@
1
- # react-form-action
2
-
3
- End-to-end typesafe success, error & validation state control for Next.js form actions.
4
-
5
- ## Features
6
-
7
- **Action Creator**
8
-
9
- - ✅ Provides envelope objects with `"initial" | "invalid" | "success" | "failure"` response types.
10
- - ✅ Define generic payload for each of the response type.
11
- - ✅ Bind action arguments.
12
-
13
- **tRPC-like Form Action builder**
14
-
15
- - ✅ Define payload schema with the `.input(zodSchema)` to validate the `formData`
16
- - ✅ Reuse business logic with the `.use(middleware)` method.
17
- - ✅ Define bindable arguments with the `.args([])` method.
18
- - ✅ Reuse error handling with the `.error(handler)`.
19
-
20
- **React Context access with the `<Action action={myFormAction} />` component**
21
-
22
- - ✅ The `useActionState()` accessible via the `useActionContext()` hook.
23
- - ✅ Computes progress flags like `isInvalid`, `isSuccess` based on the envelope type.
24
-
25
- **Context-bound `<Form />` component**
26
-
27
- - ✅ Reads the `action` from the `<Action />` context.
28
- - ✅ Opt-out from the default form reset after action submit.
29
-
30
- ## Install
31
-
32
- ```
33
- npm i react-form-action zod-form-data
34
- ```
35
-
36
- <a aria-label="NPM version" href="https://www.npmjs.com/package/react-form-action">
37
- <img alt="NPM Version" src="https://img.shields.io/npm/v/react-form-action?style=for-the-badge&labelColor=24292e">
38
- </a>
39
-
40
- ## Getting Started
41
-
42
- #### 1️⃣ Create a Server Action
43
-
44
- ```tsx
45
- // app/subscribe/action.ts
46
- "use server";
47
-
48
- import { formAction } from "react-form-action";
49
- import { z } from "zod";
50
-
51
- export const subscribeAction = formAction
52
- .input(z.object({ email: z.string().email() }))
53
- .run(async ({ input }) => {
54
- return input.email;
55
- });
56
- ```
57
-
58
- #### 2️⃣ Create a Client Form Component
59
-
60
- ```tsx
61
- // app/subscribe/form.tsx
62
- "use client";
63
-
64
- import {
65
- Form,
66
- Pending,
67
- createComponents,
68
- useActionContext,
69
- } from "react-form-action/client";
70
-
71
- import { subscribeAction } from "./action";
72
-
73
- const { FieldError, Success } = createComponents(subscribeAction);
74
-
75
- export function SubscribeForm() {
76
- const { isPending, isFailure, error, data } =
77
- useActionContext(subscribeAction);
78
-
79
- return (
80
- <Form>
81
- <Success>
82
- <p>✅ Email {data} was registered.</p>
83
- </Success>
84
- <input name="email" />
85
- {/*💡 The FieldError "name" prop supports autocompletion */}
86
- <FieldError name="email" />
87
- <button type="submit" disabled={isPending}>
88
- {isPending ? "🌀 Submitting..." : "Submit"}
89
- </button>
90
- <Pending>Please wait...</Pending>
91
- </Form>
92
- );
93
- }
94
- ```
95
-
96
- #### 3️⃣ Provide the `<Action>` context on a Page
97
-
98
- ```tsx
99
- // app/subscribe/page.tsx
100
-
101
- import { Action } from "react-form-action/client";
102
-
103
- import { subscribeAction } from "./action";
104
- import { SubscribeForm } from "./form";
105
-
106
- export default function Page() {
107
- return (
108
- <Action action={subscribeAction} initialData="">
109
- <SubscribeForm />
110
- </Action>
111
- );
112
- }
113
- ```
114
-
115
- ## Usage
116
-
117
- ### `formAction` builder
118
-
119
- The [`zod-form-data`](https://www.npmjs.com/package/zod-form-data) powered action builder.
120
-
121
- ```ts
122
- // app/actions/auth.ts
123
- "use server";
124
-
125
- import { formAction } from "react-form-action";
126
- import { z } from "zod";
127
- import { cookies } from "next/headers";
128
-
129
- const i18nMiddleware = async () => {
130
- const { t } = await useTranslation("auth", cookies().get("i18n")?.value);
131
- // will be added to context
132
- return { t };
133
- };
134
-
135
- const authAction = formAction
136
- .use(i18nMiddleware)
137
- .use(async ({ ctx: { t } }) =>
138
- console.log("🎉 context enhanced by previous middlewares 🎉", t)
139
- )
140
- .error(async ({ error }) => {
141
- if (error instanceof DbError) {
142
- return error.custom.code;
143
- } else {
144
- // unknown error
145
- // default Next.js error handling (error.js boundary)
146
- throw error;
147
- }
148
- });
149
-
150
- export const signIn = authAction
151
- .input(z.object({ email: z.string().email() }))
152
- // 🎉 extend the previous input (only without refinements and transforms)
153
- .input(z.object({ password: z.string() }))
154
- .run(async ({ ctx: { t }, input: { email, password } }) => {
155
- // Type inferred: {email: string, password: string}
156
-
157
- await db.signIn({ email, password });
158
-
159
- return t("verificationEmail.success");
160
- });
161
-
162
- export const signUp = authAction
163
- .input(
164
- z
165
- .object({
166
- email: z.string().email(),
167
- password: z.string(),
168
- confirm: z.string(),
169
- })
170
- .refine((data) => data.password === data.confirm, {
171
- message: "Passwords don't match",
172
- path: ["confirm"],
173
- })
174
- ) // if using refinement, only one input call is permited, as schema with ZodEffects is not extendable.
175
- .run(async ({ ctx: { t }, input: { email, password } }) => {
176
- // 🎉 passwords match!
177
-
178
- const tokenData = await db.signUp({ email, password });
179
-
180
- if (!tokenData) {
181
- return t("signUp.emailVerificationRequired");
182
- }
183
-
184
- return t("singUp.success");
185
- });
186
- ```
187
-
188
- #### Args binding
189
-
190
- The `formAction` builder supports action arguments binding:
191
-
192
- ```ts
193
- // app/update-user/[userId]/action.tsx
194
- import { formAction } from "react-form-action";
195
-
196
- export const updateUser = formAction
197
- .args([z.string().uuid()])
198
- .run(async ({ args: [userId] }) => {
199
- return userId;
200
- // ^? string
201
- });
202
- ```
203
-
204
- ```tsx
205
- // app/update-user/[userId]/page.tsx
206
- import { Action } from "react-form-action/client";
207
-
208
- import { updateUser } from "./action";
209
- import { UpdateUserForm } from "./form";
210
-
211
- export default function Page({
212
- params,
213
- }: {
214
- params: Promise<{ userId: string }>;
215
- }) {
216
- const { userId } = await params;
217
-
218
- const action = updateUser.bind(null, userId);
219
-
220
- return (
221
- <Action action={action} initialData="">
222
- <SubscribeForm />
223
- </Action>
224
- );
225
- }
226
- ```
227
-
228
- ### Action Creator
229
-
230
- Low-level action creator, which provides the `success`, `failure` and `invalid` envelope constructors. With the `createFormAction` you must handle the native `FormData` by yourself.
231
-
232
- ```ts
233
- "use server";
234
-
235
- import { createFormAction } from "react-form-action";
236
- import { z } from "zod";
237
-
238
- // Define custom serializable error & success data types
239
- type ErrorData = {
240
- message: string;
241
- };
242
-
243
- type SuccessData = {
244
- message: string;
245
- };
246
-
247
- type ValiationError = {
248
- name?: string;
249
- };
250
-
251
- const updateUserSchema = z.object({ name: z.string() });
252
-
253
- export const updateUser = createFormAction<
254
- SuccessData,
255
- ErrorData,
256
- ValiationError
257
- >(({ success, failure, invalid }) =>
258
- // success and failure helper functions create wrappers for success & error data respectively
259
- async (prevState, formData) => {
260
- if (prevState.type === "initial") {
261
- // use the initialData passed to <Form /> here
262
- // prevState.data === "foobar"
263
- }
264
-
265
- try {
266
- const { name } = updateUserSchema.parse({
267
- name: formData.get("name"),
268
- });
269
-
270
- const user = await updateCurrentUser(name);
271
-
272
- if (user) {
273
- // {type: "success", data: "Your profile has been updated.", error: null, validationError: null}
274
- return success({
275
- message: "Your profile has been updated.",
276
- });
277
- } else {
278
- // {type: "error", data: null, error: { message: "No current user." }, validationError: null}
279
- return failure({ message: "No current user." });
280
- }
281
- } catch (error) {
282
- if (error instanceof ZodError) {
283
- // {type: "invalid", data: null, error: null, validationError: {name: "Invalid input"}}
284
- return invalid({
285
- name: error.issues[0]?.message ?? "Validation error",
286
- });
287
- }
288
-
289
- return failure({ message: "Failed to update user." });
290
- }
291
- }
292
- );
293
- ```
294
-
295
- The action creator supports arguments binding:
296
-
297
- ```ts
298
- export const updateUser = createFormAction(
299
- (
300
- { success, failure, invalid },
301
- userId: string /* Here you can specify multiple arguments */
302
- ) =>
303
- async (prevState, formData) => {
304
- try {
305
- const { name } = updateUserSchema.parse({
306
- name: formData.get("name"),
307
- });
308
-
309
- const user = await db.users.findById(userId);
310
-
311
- if (!user) {
312
- return failure({ message: "No such user." });
313
- }
314
-
315
- const updated = await user.update({ name });
316
-
317
- if (updated) {
318
- return success({
319
- message: "User has been updated.",
320
- });
321
- } else {
322
- return failure({ message: "Failed to update." });
323
- }
324
- } catch (error) {
325
- // handle error
326
- }
327
- }
328
- );
329
-
330
- // call bind as usuall, the "123" becomes the "userId"
331
- updateUser.bind(null, "123");
332
- ```
333
-
334
- ### Action Context
335
-
336
- The `<Action>` components enables you to access your `action`'s state with the `useActionContext()` hook:
337
-
338
- ```tsx
339
- // 👉 Define standalone client form component (e.g. /app/auth/signup/SignUpForm.tsx)
340
- "use client";
341
-
342
- import { Action, Form, useActionContext } from "react-form-action/client";
343
- import type { PropsWithChildren } from "react";
344
-
345
- import { signupAction } from "./action";
346
-
347
- function Pending({ children }: PropsWithChildren) {
348
- // read any state from the ActionContext:
349
- const {
350
- error,
351
- data,
352
- validationError,
353
- isPending,
354
- isFailure,
355
- isInvalid,
356
- isSuccess,
357
- isInitial,
358
- } = useActionContext();
359
-
360
- return isPending && children;
361
- }
362
-
363
- // 💡 render this form on your RSC page (/app/auth/signup/page.tsx)
364
- export function SignupForm() {
365
- return (
366
- <Action action={signupAction}>
367
- <Form>
368
- <input name="email" />
369
- <input name="password" />
370
- </Form>
371
- {/* 🎆 Read the pending state outside the <Form> */}
372
- <Pending>
373
- {/* This renders only when the action is pending. 😎 */}
374
- <p>Please wait...</p>
375
- </Pending>
376
- </Action>
377
- );
378
- }
379
- ```
380
-
381
- ### `<Form>` Component
382
-
383
- The `<form>` submits the action in `onSubmit` handler to [prevent automatic form reset](https://github.com/facebook/react/issues/29034).
384
- Pass `autoReset` prop to use the `action` prop instead and keep the default reset.
385
-
386
- ```tsx
387
- "use client";
388
-
389
- import { Action, Form } from "react-form-action/client";
390
-
391
- import { updateUser } from "./action";
392
-
393
- export function UpdateUserForm() {
394
- return (
395
- <Action action={updateUser}>
396
- <Form autoReset>{/* ... */}</Form>
397
- </Action>
398
- );
399
- }
400
- ```
401
-
402
- ### Context Bound Components `createComponents()`
403
-
404
- Use the `createComponents(action)` helper to create components which use the ActionContext and have types bound to the action type.
405
-
406
- #### `<FielError>` Component
407
-
408
- ```tsx
409
- "use client";
410
-
411
- // ⚠️ createComponents is usable only in "use client" components
412
- import { Form, createComponents } from "react-form-action/client";
413
-
414
- import { authAction } from "./actions";
415
-
416
- export const signUpAction = authAction
417
- .input(
418
- z
419
- .object({
420
- user: z.object({
421
- email: z.string().email(),
422
- name: z.string(),
423
- }),
424
- password: z.string().min(8),
425
- confirm: z.string(),
426
- })
427
- .refine((data) => data.password === data.confirm, {
428
- message: "Passwords don't match",
429
- })
430
- )
431
- .run(async ({ ctx, input }) => {
432
- return null;
433
- });
434
-
435
- // 🌟 The FieldError is now bound do the signUpAction input schema which allows autocompletion for its "name" prop
436
- // ⚠️ Usable only with actions created with the formAction builder
437
- const { FieldError } = createComponents(signUpAction);
438
-
439
- export function SignUpForm() {
440
- return (
441
- <Action action={signUpAction} initialData={null}>
442
- <Form>
443
- {/* 1️⃣ When the "name" prop is ommited, the top-level error will be rendered e.g.:
444
- "Passwords don't match" */}
445
- <FieldError />
446
- {/* 2️⃣ Access fields by their name: */}
447
- <FieldError name="password" />
448
- {/* 3️⃣ Access nested fields by dot access notation: */}
449
- <FieldError name="user.email" />
450
- </Form>
451
- </Action>
452
- );
453
- }
454
- ```
455
-
456
- #### `<Success>`
457
-
458
- #### When children are JSX
459
-
460
- ```tsx
461
- import { Action, createComponents } from "react-form-action/client";
462
-
463
- const { Success } = createComponents(signUpAction);
464
-
465
- function MyForm() {
466
- return (
467
- <Action action={signUpAction}>
468
- <Success>
469
- {/* 👉 The message will render only after the action has succeeded */}
470
- <p>You've been signed up!</p>
471
- </Success>
472
- </Action>
473
- );
474
- }
475
- ```
476
-
477
- #### When children is a render prop
478
-
479
- ```tsx
480
- import { createComponents } from "react-form-action/client";
481
-
482
- const { Success } = createComponents(signUpAction);
483
-
484
- function Label({children}: PropsWithChildren) {
485
- return (
486
- <Success>
487
- {({ isSuccess, data }) => (
488
- {/* 👉 With a render prop, the children are always mounted, regardles of the isSuccess flag */}
489
- <label className={isSuccess ? "green" : ""}>
490
- {children}
491
- </label>
492
- )}
493
- </Success>
494
- );
495
- };
496
- ```
497
-
498
- ### `<Pending>`
499
-
500
- Render children when the action is pending:
501
-
502
- #### When children are JSX
503
-
504
- ```tsx
505
- import { Action, Pending } from "react-form-action/client";
506
-
507
- import { Spinner } from "./components";
508
-
509
- function MyForm() {
510
- return (
511
- <Action action={action}>
512
- {/* 👉 Unlike the React.useFormStatus() hook, we don't need here the <form> element at all. */}
513
- <Pending>
514
- {/* 👉 The spinner will UNMOUNT when the action is NOT pending */}
515
- <Spinner />
516
- </Pending>
517
- </Action>
518
- );
519
- }
520
- ```
521
-
522
- #### When children is a render prop
523
-
524
- ```tsx
525
- import { Pending } from "react-form-action/client";
526
-
527
- function SubmitButton() {
528
- return (
529
- <Pending>
530
- {({ isPending }) => (
531
- {/* 👉 With a render prop, the children are always mounted, regardles of the isPending flag */}
532
- <button type="submit" disabled={isPending}>
533
- {isPending ? "Submitting..." : "Submit"}
534
- </button>
535
- )}
536
- </Pending>
537
- );
538
- };
539
- ```
1
+ # react-form-action
2
+
3
+ End-to-end typesafe success, error & validation state control for Next.js form actions.
4
+
5
+ ## Features
6
+
7
+ **Action Creator**
8
+
9
+ - ✅ Provides envelope objects with `"initial" | "invalid" | "success" | "failure"` response types.
10
+ - ✅ Define generic payload for each of the response type.
11
+ - ✅ Bind action arguments.
12
+
13
+ **tRPC-like Form Action builder**
14
+
15
+ - ✅ Define payload schema with the `.input(zodSchema)` to validate the `formData`
16
+ - ✅ Reuse business logic with the `.use(middleware)` method.
17
+ - ✅ Define bindable arguments with the `.args([])` method.
18
+ - ✅ Reuse error handling with the `.error(handler)`.
19
+
20
+ **React Context access with the `<Action action={myFormAction} />` component**
21
+
22
+ - ✅ The `useActionState()` accessible via the `useActionContext()` hook.
23
+ - ✅ Computes progress flags like `isInvalid`, `isSuccess` based on the envelope type.
24
+
25
+ **Context-bound `<Form />` component**
26
+
27
+ - ✅ Reads the `action` from the `<Action />` context.
28
+ - ✅ Opt-out from the default form reset after action submit.
29
+
30
+ ## Install
31
+
32
+ ```
33
+ npm i react-form-action zod-form-data
34
+ ```
35
+
36
+ <a aria-label="NPM version" href="https://www.npmjs.com/package/react-form-action">
37
+ <img alt="NPM Version" src="https://img.shields.io/npm/v/react-form-action?style=for-the-badge&labelColor=24292e">
38
+ </a>
39
+
40
+ ## Getting Started
41
+
42
+ #### 1️⃣ Create a Server Action
43
+
44
+ ```tsx
45
+ // app/subscribe/action.ts
46
+ "use server";
47
+
48
+ import { formAction } from "react-form-action";
49
+ import { z } from "zod";
50
+
51
+ export const subscribeAction = formAction
52
+ .input(z.object({ email: z.string().email() }))
53
+ .run(async ({ input }) => {
54
+ return input.email;
55
+ });
56
+ ```
57
+
58
+ #### 2️⃣ Create a Client Form Component
59
+
60
+ ```tsx
61
+ // app/subscribe/form.tsx
62
+ "use client";
63
+
64
+ import {
65
+ Form,
66
+ Pending,
67
+ createComponents,
68
+ useActionContext,
69
+ } from "react-form-action/client";
70
+
71
+ import { subscribeAction } from "./action";
72
+
73
+ const { FieldError, Success } = createComponents(subscribeAction);
74
+
75
+ export function SubscribeForm() {
76
+ const { isPending, isFailure, error, data } =
77
+ useActionContext(subscribeAction);
78
+
79
+ return (
80
+ <Form>
81
+ <Success>
82
+ <p>✅ Email {data} was registered.</p>
83
+ </Success>
84
+ {/*💡 The FieldError "name" prop supports autocompletion */}
85
+ <FieldError name="email">
86
+ {({ name, error }) => (
87
+ <input className={error ? "invalid" : ""} name={name} />
88
+ )}
89
+ </FieldError>
90
+ <button type="submit" disabled={isPending}>
91
+ {isPending ? "🌀 Submitting..." : "Submit"}
92
+ </button>
93
+ <Pending>Please wait...</Pending>
94
+ </Form>
95
+ );
96
+ }
97
+ ```
98
+
99
+ #### 3️⃣ Provide the `<Action>` context on a Page
100
+
101
+ ```tsx
102
+ // app/subscribe/page.tsx
103
+
104
+ import { Action } from "react-form-action/client";
105
+
106
+ import { subscribeAction } from "./action";
107
+ import { SubscribeForm } from "./form";
108
+
109
+ export default function Page() {
110
+ return (
111
+ <Action action={subscribeAction} initialData="">
112
+ <SubscribeForm />
113
+ </Action>
114
+ );
115
+ }
116
+ ```
117
+
118
+ ## Usage
119
+
120
+ ### `formAction` builder
121
+
122
+ The [`zod-form-data`](https://www.npmjs.com/package/zod-form-data) powered action builder.
123
+
124
+ ```ts
125
+ // app/actions/auth.ts
126
+ "use server";
127
+
128
+ import { formAction } from "react-form-action";
129
+ import { z } from "zod";
130
+ import { cookies } from "next/headers";
131
+
132
+ const i18nMiddleware = async () => {
133
+ const { t } = await useTranslation("auth", cookies().get("i18n")?.value);
134
+ // will be added to context
135
+ return { t };
136
+ };
137
+
138
+ const authAction = formAction
139
+ .use(i18nMiddleware)
140
+ .use(async ({ ctx: { t } }) =>
141
+ console.log("🎉 context enhanced by previous middlewares 🎉", t)
142
+ )
143
+ .error(async ({ error }) => {
144
+ if (error instanceof DbError) {
145
+ return error.custom.code;
146
+ } else {
147
+ // unknown error
148
+ // default Next.js error handling (error.js boundary)
149
+ throw error;
150
+ }
151
+ });
152
+
153
+ export const signIn = authAction
154
+ .input(z.object({ email: z.string().email() }))
155
+ // 🎉 extend the previous input (only without refinements and transforms)
156
+ .input(z.object({ password: z.string() }))
157
+ .run(async ({ ctx: { t }, input: { email, password } }) => {
158
+ // Type inferred: {email: string, password: string}
159
+
160
+ await db.signIn({ email, password });
161
+
162
+ return t("verificationEmail.success");
163
+ });
164
+
165
+ export const signUp = authAction
166
+ .input(
167
+ z
168
+ .object({
169
+ email: z.string().email(),
170
+ password: z.string(),
171
+ confirm: z.string(),
172
+ })
173
+ .refine((data) => data.password === data.confirm, {
174
+ message: "Passwords don't match",
175
+ path: ["confirm"],
176
+ })
177
+ ) // if using refinement, only one input call is permited, as schema with ZodEffects is not extendable.
178
+ .run(async ({ ctx: { t }, input: { email, password } }) => {
179
+ // 🎉 passwords match!
180
+
181
+ const tokenData = await db.signUp({ email, password });
182
+
183
+ if (!tokenData) {
184
+ return t("signUp.emailVerificationRequired");
185
+ }
186
+
187
+ return t("singUp.success");
188
+ });
189
+ ```
190
+
191
+ #### Args binding
192
+
193
+ The `formAction` builder supports action arguments binding:
194
+
195
+ ```ts
196
+ // app/update-user/[userId]/action.tsx
197
+ import { formAction } from "react-form-action";
198
+
199
+ export const updateUser = formAction
200
+ .args([z.string().uuid()])
201
+ .run(async ({ args: [userId] }) => {
202
+ return userId;
203
+ // ^? string
204
+ });
205
+ ```
206
+
207
+ ```tsx
208
+ // app/update-user/[userId]/page.tsx
209
+ import { Action } from "react-form-action/client";
210
+
211
+ import { updateUser } from "./action";
212
+ import { UpdateUserForm } from "./form";
213
+
214
+ export default function Page({
215
+ params,
216
+ }: {
217
+ params: Promise<{ userId: string }>;
218
+ }) {
219
+ const { userId } = await params;
220
+
221
+ const action = updateUser.bind(null, userId);
222
+
223
+ return (
224
+ <Action action={action} initialData="">
225
+ <SubscribeForm />
226
+ </Action>
227
+ );
228
+ }
229
+ ```
230
+
231
+ ### Action Creator
232
+
233
+ Low-level action creator, which provides the `success`, `failure` and `invalid` envelope constructors. With the `createFormAction` you must handle the native `FormData` by yourself.
234
+
235
+ ```ts
236
+ "use server";
237
+
238
+ import { createFormAction } from "react-form-action";
239
+ import { z } from "zod";
240
+
241
+ // Define custom serializable error & success data types
242
+ type ErrorData = {
243
+ message: string;
244
+ };
245
+
246
+ type SuccessData = {
247
+ message: string;
248
+ };
249
+
250
+ type ValiationError = {
251
+ name?: string;
252
+ };
253
+
254
+ const updateUserSchema = z.object({ name: z.string() });
255
+
256
+ export const updateUser = createFormAction<
257
+ SuccessData,
258
+ ErrorData,
259
+ ValiationError
260
+ >(({ success, failure, invalid }) =>
261
+ // success and failure helper functions create wrappers for success & error data respectively
262
+ async (prevState, formData) => {
263
+ if (prevState.type === "initial") {
264
+ // use the initialData passed to <Form /> here
265
+ // prevState.data === "foobar"
266
+ }
267
+
268
+ try {
269
+ const { name } = updateUserSchema.parse({
270
+ name: formData.get("name"),
271
+ });
272
+
273
+ const user = await updateCurrentUser(name);
274
+
275
+ if (user) {
276
+ // {type: "success", data: "Your profile has been updated.", error: null, validationError: null}
277
+ return success({
278
+ message: "Your profile has been updated.",
279
+ });
280
+ } else {
281
+ // {type: "error", data: null, error: { message: "No current user." }, validationError: null}
282
+ return failure({ message: "No current user." });
283
+ }
284
+ } catch (error) {
285
+ if (error instanceof ZodError) {
286
+ // {type: "invalid", data: null, error: null, validationError: {name: "Invalid input"}}
287
+ return invalid({
288
+ name: error.issues[0]?.message ?? "Validation error",
289
+ });
290
+ }
291
+
292
+ return failure({ message: "Failed to update user." });
293
+ }
294
+ }
295
+ );
296
+ ```
297
+
298
+ The action creator supports arguments binding:
299
+
300
+ ```ts
301
+ export const updateUser = createFormAction(
302
+ (
303
+ { success, failure, invalid },
304
+ userId: string /* Here you can specify multiple arguments */
305
+ ) =>
306
+ async (prevState, formData) => {
307
+ try {
308
+ const { name } = updateUserSchema.parse({
309
+ name: formData.get("name"),
310
+ });
311
+
312
+ const user = await db.users.findById(userId);
313
+
314
+ if (!user) {
315
+ return failure({ message: "No such user." });
316
+ }
317
+
318
+ const updated = await user.update({ name });
319
+
320
+ if (updated) {
321
+ return success({
322
+ message: "User has been updated.",
323
+ });
324
+ } else {
325
+ return failure({ message: "Failed to update." });
326
+ }
327
+ } catch (error) {
328
+ // handle error
329
+ }
330
+ }
331
+ );
332
+
333
+ // call bind as usuall, the "123" becomes the "userId"
334
+ updateUser.bind(null, "123");
335
+ ```
336
+
337
+ ### Action Context
338
+
339
+ The `<Action>` components enables you to access your `action`'s state with the `useActionContext()` hook:
340
+
341
+ ```tsx
342
+ // 👉 Define standalone client form component (e.g. /app/auth/signup/SignUpForm.tsx)
343
+ "use client";
344
+
345
+ import { Action, Form, useActionContext } from "react-form-action/client";
346
+ import type { PropsWithChildren } from "react";
347
+
348
+ import { signupAction } from "./action";
349
+
350
+ function Pending({ children }: PropsWithChildren) {
351
+ // read any state from the ActionContext:
352
+ const {
353
+ error,
354
+ data,
355
+ validationError,
356
+ isPending,
357
+ isFailure,
358
+ isInvalid,
359
+ isSuccess,
360
+ isInitial,
361
+ } = useActionContext();
362
+
363
+ return isPending && children;
364
+ }
365
+
366
+ // 💡 render this form on your RSC page (/app/auth/signup/page.tsx)
367
+ export function SignupForm() {
368
+ return (
369
+ <Action action={signupAction}>
370
+ <Form>
371
+ <input name="email" />
372
+ <input name="password" />
373
+ </Form>
374
+ {/* 🎆 Read the pending state outside the <Form> */}
375
+ <Pending>
376
+ {/* This renders only when the action is pending. 😎 */}
377
+ <p>Please wait...</p>
378
+ </Pending>
379
+ </Action>
380
+ );
381
+ }
382
+ ```
383
+
384
+ ### `<Form>` Component
385
+
386
+ The `<form>` submits the action in `onSubmit` handler to [prevent automatic form reset](https://github.com/facebook/react/issues/29034).
387
+ Pass `autoReset` prop to use the `action` prop instead and keep the default reset.
388
+
389
+ ```tsx
390
+ "use client";
391
+
392
+ import { Action, Form } from "react-form-action/client";
393
+
394
+ import { updateUser } from "./action";
395
+
396
+ export function UpdateUserForm() {
397
+ return (
398
+ <Action action={updateUser}>
399
+ <Form autoReset>{/* ... */}</Form>
400
+ </Action>
401
+ );
402
+ }
403
+ ```
404
+
405
+ ### Context Bound Components `createComponents()`
406
+
407
+ Use the `createComponents(action)` helper to create components which use the ActionContext and have types bound to the action type.
408
+
409
+ #### `<FielError>` Component
410
+
411
+ ```tsx
412
+ "use client";
413
+
414
+ // ⚠️ createComponents is usable only in "use client" components
415
+ import { Form, createComponents } from "react-form-action/client";
416
+
417
+ import { authAction } from "./actions";
418
+
419
+ export const signUpAction = authAction
420
+ .input(
421
+ z
422
+ .object({
423
+ user: z.object({
424
+ email: z.string().email(),
425
+ name: z.string(),
426
+ }),
427
+ password: z.string().min(8),
428
+ confirm: z.string(),
429
+ })
430
+ .refine((data) => data.password === data.confirm, {
431
+ message: "Passwords don't match",
432
+ })
433
+ )
434
+ .run(async ({ ctx, input }) => {
435
+ return null;
436
+ });
437
+
438
+ // 🌟 The FieldError is now bound do the signUpAction input schema which allows autocompletion for its "name" prop
439
+ // ⚠️ Usable only with actions created with the formAction builder
440
+ const { FieldError } = createComponents(signUpAction);
441
+
442
+ export function SignUpForm() {
443
+ return (
444
+ <Action action={signUpAction} initialData={null}>
445
+ <Form>
446
+ {/* 1️⃣ When the "name" prop is an empty string, the top-level error will be rendered e.g.:
447
+ "Passwords don't match" */}
448
+ <FieldError name="" />
449
+ {/* 2️⃣ Access fields by their name: */}
450
+ <FieldError name="password" />
451
+ <FieldError name="confirm" />
452
+ {/* 3️⃣ Access nested fields by dot access notation: */}
453
+ <FieldError name="user.email" />
454
+ </Form>
455
+ </Action>
456
+ );
457
+ }
458
+ ```
459
+
460
+ #### `<Success>`
461
+
462
+ #### When children are JSX
463
+
464
+ ```tsx
465
+ import { Action, createComponents } from "react-form-action/client";
466
+
467
+ const { Success } = createComponents(signUpAction);
468
+
469
+ function MyForm() {
470
+ return (
471
+ <Action action={signUpAction}>
472
+ <Success>
473
+ {/* 👉 The message will render only after the action has succeeded */}
474
+ <p>You've been signed up!</p>
475
+ </Success>
476
+ </Action>
477
+ );
478
+ }
479
+ ```
480
+
481
+ #### When children is a render prop
482
+
483
+ ```tsx
484
+ import { createComponents } from "react-form-action/client";
485
+
486
+ const { Success } = createComponents(signUpAction);
487
+
488
+ function Label({children}: PropsWithChildren) {
489
+ return (
490
+ <Success>
491
+ {({ isSuccess, data }) => (
492
+ {/* 👉 With a render prop, the children are always mounted, regardles of the isSuccess flag */}
493
+ <label className={isSuccess ? "green" : ""}>
494
+ {children}
495
+ </label>
496
+ )}
497
+ </Success>
498
+ );
499
+ };
500
+ ```
501
+
502
+ ### `<Pending>`
503
+
504
+ Render children when the action is pending:
505
+
506
+ #### When children are JSX
507
+
508
+ ```tsx
509
+ import { Action, Pending } from "react-form-action/client";
510
+
511
+ import { Spinner } from "./components";
512
+
513
+ function MyForm() {
514
+ return (
515
+ <Action action={action}>
516
+ {/* 👉 Unlike the React.useFormStatus() hook, we don't need here the <form> element at all. */}
517
+ <Pending>
518
+ {/* 👉 The spinner will UNMOUNT when the action is NOT pending */}
519
+ <Spinner />
520
+ </Pending>
521
+ </Action>
522
+ );
523
+ }
524
+ ```
525
+
526
+ #### When children is a render prop
527
+
528
+ ```tsx
529
+ import { Pending } from "react-form-action/client";
530
+
531
+ function SubmitButton() {
532
+ return (
533
+ <Pending>
534
+ {({ isPending }) => (
535
+ {/* 👉 With a render prop, the children are always mounted, regardles of the isPending flag */}
536
+ <button type="submit" disabled={isPending}>
537
+ {isPending ? "Submitting..." : "Submit"}
538
+ </button>
539
+ )}
540
+ </Pending>
541
+ );
542
+ };
543
+ ```
package/dist/index.d.mts CHANGED
@@ -29,9 +29,9 @@ type SchemaAction<Data, Context, Args extends AnyZodTuple, Schema extends ZodSch
29
29
  input: z.infer<Schema>;
30
30
  }) => Promise<Data>;
31
31
  type AnyZodEffects = ZodEffects<any, any, any>;
32
- type FormActionBuilder<Schema extends ZodTypeAny | EmptyInput, Err = Error, Context = InitialContext, Args extends AnyZodTuple = ZodTuple<[], null>> = {
33
- args: <T extends [ZodTypeAny, ...ZodTypeAny[]]>(args: T) => FormActionBuilder<Schema, Err, Context, ZodTuple<T, null>>;
34
- input: <T extends ZodTypeAny>(newInput: T extends ZodType<infer Out extends Record<string, unknown>> ? Schema extends AnyZodEffects ? "Extending schema with effect is not possible." : Schema extends EmptyInput ? T : Schema extends ZodTypeAny ? T extends AnyZodEffects ? "Your input contains effect which prevents merging it with the previous inputs." : T : T : "The schema output must be an object.") => FormActionBuilder<Schema extends ZodObject<infer O1 extends ZodRawShape> ? T extends ZodObject<infer O2 extends ZodRawShape> ? ZodObject<objectUtil.extendShape<O1, O2>> : never : T, Err, Context, Args>;
32
+ type FormActionBuilder<Schema extends ZodTypeAny | EmptyInput, TErr = unknown, Context = InitialContext, Args extends AnyZodTuple = ZodTuple<[], null>> = {
33
+ args: <T extends [ZodTypeAny, ...ZodTypeAny[]]>(args: T) => FormActionBuilder<Schema, TErr, Context, ZodTuple<T, null>>;
34
+ input: <T extends ZodTypeAny>(newInput: T extends ZodType<infer Out extends Record<string, unknown>> ? Schema extends AnyZodEffects ? "Extending schema with effect is not possible." : Schema extends EmptyInput ? T : Schema extends ZodTypeAny ? T extends AnyZodEffects ? "Your input contains effect which prevents merging it with the previous inputs." : T : T : "The schema output must be an object.") => FormActionBuilder<Schema extends ZodObject<infer O1 extends ZodRawShape> ? T extends ZodObject<infer O2 extends ZodRawShape> ? ZodObject<objectUtil.extendShape<O1, O2>> : never : T, TErr, Context, Args>;
35
35
  /**
36
36
  * A finishing call on the builder completing your action.
37
37
  * @param action async function to execute. It will receive parsed formData as input, when the builder was initialized with a schema.
@@ -39,13 +39,13 @@ type FormActionBuilder<Schema extends ZodTypeAny | EmptyInput, Err = Error, Cont
39
39
  */
40
40
  run: Schema extends ZodTypeAny ? <Data>(action: SchemaAction<Data, Flatten<Context>, Args, Schema>) => (...args: [
41
41
  ...z.infer<Args>,
42
- state: ActionState<Data, Error, z.inferFormattedError<Schema> & z.inferFormattedError<Args>>,
42
+ state: ActionState<Data, TErr, z.inferFormattedError<Schema> & z.inferFormattedError<Args>>,
43
43
  payload: FormData
44
- ]) => Promise<Flatten<InvalidState<z.inferFormattedError<Schema> & z.inferFormattedError<Args>> | FailureState<Err> | SuccessState<Data>>> : <Data>(action: Action<Data, Flatten<Context>, Args>) => (...args: [
44
+ ]) => Promise<Flatten<InvalidState<z.inferFormattedError<Schema> & z.inferFormattedError<Args>> | FailureState<TErr> | SuccessState<Data>>> : <Data>(action: Action<Data, Flatten<Context>, Args>) => (...args: [
45
45
  ...z.infer<Args>,
46
- state: ActionState<Data, Error, z.inferFormattedError<Args>>,
46
+ state: ActionState<Data, TErr, z.inferFormattedError<Args>>,
47
47
  payload: FormData
48
- ]) => Promise<Flatten<InvalidState<z.inferFormattedError<Args>> | FailureState<Err> | SuccessState<Data>>>;
48
+ ]) => Promise<Flatten<InvalidState<z.inferFormattedError<Args>> | FailureState<TErr> | SuccessState<Data>>>;
49
49
  /**
50
50
  * A chainable context enhancing helper.
51
51
  * @param middleware A function which receives the current context, and returns an object which will be added to the context.
@@ -53,15 +53,15 @@ type FormActionBuilder<Schema extends ZodTypeAny | EmptyInput, Err = Error, Cont
53
53
  */
54
54
  use: <NewContext extends Record<string, unknown>>(middleware: ({ ctx }: {
55
55
  ctx: Context;
56
- }) => Promise<NewContext>) => FormActionBuilder<Schema, Err, Context & NewContext, Args>;
56
+ }) => Promise<NewContext>) => FormActionBuilder<Schema, TErr, Context & NewContext, Args>;
57
57
  /**
58
58
  * A chainable error handler to handle errors thrown while running the action passed to .run().
59
59
  * It does not receive errors thrown from the context/middleware.
60
60
  * @param processError A function which receives action's errors.
61
61
  * @returns FormActionBuilder
62
62
  */
63
- error: <Err>(processError: ErrorHandler<Context, Err>) => FormActionBuilder<Schema, Err, Context, Args>;
63
+ error: <TErr>(processError: ErrorHandler<Context, TErr>) => FormActionBuilder<Schema, TErr, Context, Args>;
64
64
  };
65
- declare const formAction: FormActionBuilder<typeof emptyInput, Error, InitialContext, z.ZodTuple<[], null>>;
65
+ declare const formAction: FormActionBuilder<typeof emptyInput, unknown, InitialContext, z.ZodTuple<[], null>>;
66
66
 
67
67
  export { ActionState, FailureState, InvalidState, SuccessState, formAction };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{a as w,b as y}from"./chunk-LPB4R2UB.mjs";import{z as S,ZodFirstPartyTypeKind as h}from"zod";import{zfd as C}from"zod-form-data";var x=Symbol();function c(e,s=[],o,a){async function A(t){let r={formData:t};for(let d of s){let i=await d({ctx:r});r={...r,...i}}return r}return{args(t){return c(e,s,o,S.tuple(t))},input(t){if(e===x)return c(t,s,o,a);if(e._def.effect)throw new Error("Previous input is not augmentable because it contains an effect.");if(t._def.effect)throw new Error("Your input contains effect which prevents merging it with the previous inputs.");if(e._def.typeName===h.ZodObject){let r=e.merge(t);return c(r,s)}else throw Error("Merging inputs works only for object schemas without effects.")},use(t){return c(e,[...s,t],o,a)},error(t){return c(e,s,t,a)},run:e===x?t=>y(({success:r,failure:d,invalid:i},...u)=>async(l,E)=>{let m=await A(E);if(a){let n=a.safeParse(u);if(!n.success)return i(n.error.format())}try{return r(await t({args:u,ctx:m}))}catch(n){if(o)return d(await o({error:n,ctx:m}));throw n}}):e===x?void 0:t=>y(({success:r,failure:d,invalid:i},...u)=>{let l=C.formData(e);return async(E,m)=>{let n=await A(m);if(a){let f=a.safeParse(u);if(!f.success)return i(f.error.format())}let p=l.safeParse(m);if(!p.success)return i(p.error.format());let g=p.data;try{return r(await t({args:u,input:g,ctx:n}))}catch(f){if(o)return d(await o({error:f,ctx:n}));throw f}}})}}var k=c(x);export{y as createFormAction,k as formAction,w as initial};
1
+ import{a as Z,b as y}from"./chunk-LPB4R2UB.mjs";import{z as g,ZodFirstPartyTypeKind as S}from"zod";import{zfd as h}from"zod-form-data";var x=Symbol();function c(e,s=[],o,a){async function A(t){let r={formData:t};for(let d of s){let i=await d({ctx:r});r={...r,...i}}return r}return{args(t){return c(e,s,o,g.tuple(t))},input(t){if(e===x)return c(t,s,o,a);if(e._def.effect)throw new Error("Previous input is not augmentable because it contains an effect.");if(t._def.effect)throw new Error("Your input contains effect which prevents merging it with the previous inputs.");if(e._def.typeName===S.ZodObject){let r=e.merge(t);return c(r,s)}else throw Error("Merging inputs works only for object schemas without effects.")},use(t){return c(e,[...s,t],o,a)},error(t){return c(e,s,t,a)},run:e===x?t=>y(({success:r,failure:d,invalid:i},...u)=>async(T,l)=>{let m=await A(l);if(a){let n=a.safeParse(u);if(!n.success)return i(n.error.format())}try{return r(await t({args:u,ctx:m}))}catch(n){if(o)return d(await o({error:n,ctx:m}));throw n}}):e===x?void 0:t=>y(({success:r,failure:d,invalid:i},...u)=>{let T=h.formData(e);return async(l,m)=>{let n=await A(m);if(a){let f=a.safeParse(u);if(!f.success)return i(f.error.format())}let p=T.safeParse(m);if(!p.success)return i(p.error.format());let E=p.data;try{return r(await t({args:u,input:E,ctx:n}))}catch(f){if(o)return d(await o({error:f,ctx:n}));throw f}}})}}var k=c(x);export{y as createFormAction,k as formAction,Z as initial};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-action",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "State management helpers for the react form actions.",
5
5
  "repository": {
6
6
  "type": "git",