react-form-action 2.3.0 → 2.4.1

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