However if you do it this way you will not have access to the data and error variables. To get the data and error you can use the useServerAction hook.
In this example, we create a basic form and handle the form submission manually. We use the useServerAction hook to execute the ZSA action and handle the success and error states accordingly.
The useServerAction hook from the zsa-react library allows you to easily integrate ZSA with the popular react-hook-form library. Here's an example of how to use it:
actions.ts
"use server"import z from "zod"import { createServerAction } from "zsa"export const produceNewMessage = createServerAction() .input( z.object({ name: z.string().min(5), }), ) .handler(async ({ input }) => { await new Promise((resolve) => setTimeout(resolve, 500)) return "Hello, " + input.name })
Then on the client side, you can use the useServerAction hook to execute the action and handle the result.
react-hook-form.tsx
"use client"import { Button } from "@/components/ui/button"import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage,} from "@/components/ui/form"import { Input } from "@/components/ui/input"import { zodResolver } from "@hookform/resolvers/zod"import { useForm } from "react-hook-form"import { z } from "zod"import { useServerAction } from "zsa-react"import { produceNewMessage } from "./actions"const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters.", }),})export function ReactHookForm() { const { isPending, execute, data, error } = useServerAction(produceNewMessage) const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { name: "", }, }) async function onSubmit(values: z.infer<typeof formSchema>) { const [data, err] = await execute(values) if (err) { // show a toast or something return } form.reset({ name: "" }) } return ( <Card className="not-prose"> <CardHeader> <CardTitle>Form Example</CardTitle> </CardHeader> <CardContent className="flex flex-col gap-4"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input placeholder="shadcn" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button disabled={isPending} type="submit" className="w-full"> {isPending ? "Saving..." : "Save"} </Button> </form> </Form> {data && <div>Message: {data}</div>} {error && <div>Error: {JSON.stringify(error.fieldErrors)}</div>} </CardContent> </Card> )}
Here is the result:
Form Example
In this example, we define a form schema using Zod and create a form using react-hook-form. We then use the useServerAction hook to execute the ZSA action when the form is submitted. The isPending variable can be used to display a loading state while the action is being executed.
useActionState was previously called useFormState. It is now deprecated and will be removed in a future release.
The useActionState hook from ZSA allows you to perform actions and manage the action state. Make sure to set the type to "state" to indicate that the input is a (previousState, formData) tuple.
This example supports progressive enhancement. Give it a shot, try submitting after disabling Javascript!
In this example, we use the useActionState hook to perform the produceNewMessage action and manage the action state. The submitAction function is passed to the form's action prop to handle the form submission. The isPending, data, and err variables can be used to display loading, success, and error states respectively.
What if you want to receive a raw payload, such as FormData object, and handle it yourself? This is useful for example when using the @conform-to/react library. Let's see how we can do that.
actions.ts
"use server"import z from "zod"import { createServerAction } from "zsa"export const produceNewMessage = createServerAction() .input( z.custom<FormData>(), { type: "state", skipInputParsing: true } ) .handler(async ({ input }) => { const payload = Object.fromEntries(input) const result = z.object({ name: z.string().min(5) }).safeParse(payload) if (result.error) { throw result.error } await new Promise((resolve) => setTimeout(resolve, 500)) return "Hello, " + result.data.name })
Then on the client side, you can use the useActionState hook to execute the action and handle the result.
ZSA supports file and/or image uploads within forms. To handle file uploads, you need to specify the expected file type in the server action using z.instanceof(File) and include a file input in your form.
actions.ts
"use server"import { z } from "zod";import { createServerAction } from "zsa";export const uploadFile = createServerAction() .input( z.object({ name: z.string().min(5), file: z .instanceof(File) .refine( (file) => file.size > 0 && file.size < 1024, "File size must be less than 1kb" ), }), { type: "state", } ) .handler(async ({ input }) => { // Process the uploaded file const { name, file } = input; // ... return "File uploaded successfully!"; });
On the client side, include a file input in your form:
In this example, we define a server action uploadFile that expects a name field of type string and a file field of type File. The type is set to "formData" to handle the form data.
On the client side, we include a file input with the name "file" in the form. When the form is submitted, the file is included in the FormData object and sent to the server action for processing.
Remember to handle the uploaded file appropriately in the server action's handler function based on your specific requirements.
That's it! You can now upload files using ZSA with forms.
ZSA also supports multi entry forms. This means that you can have multiple input fields with the same name in a form. To handle multi entry forms, you can set the schema to an array of the expected type.
Here's an example of how to create a multi entry form using ZSA:
actions.ts
"use server"import z from "zod"import { createServerAction } from "zsa"export const multiplyNumbersAction = createServerAction() .input( z.object({ // an array of numbers number: z.array(z.coerce.number()), // an array of files filefield: z .array( z .instanceof(File) .refine( (file) => file.size > 0, "File cannot be empty" ) .refine( (file) => file.size < 1024, "File size must be less than 1kb" ) ) }), { type: "state", } ) .handler(async ({ input }) => { await new Promise((resolve) => setTimeout(resolve, 500)) return ( input.number.reduce((a, b) => a * b, 1) + ` and got ${input.filefield.length} files` ) })
Then on the client side, you can use the useActionState hook to handle the multi entry form:
Sometimes you may want to bind arguments to the server action. This can be useful for passing additional data to the server action that won't be in your form. You can do this by passing an object to the bind option in the useServerAction hook.
If the action is a form action, you can also send more fields in the second argument of the execute function. This is useful for sending additional fields that are not part of the form.