Forms Learn how to use ZSA with forms.
You can use a server action directly with a regular form. Set the type
to "formData"
to indicate that the input is a FormData
object.
"use server"
import z from "zod"
import { createServerAction } from "zsa"
export const produceNewMessage = createServerAction ()
. input (
z. object ({
name: z. string (). min ( 5 ),
}),
{
type: "formData" ,
}
)
. handler ( async ({ input }) => {
await new Promise (( resolve ) => setTimeout (resolve, 500 ))
return "Hello, " + input.name
})
Then on the client you can pass the action
prop to the form to handle the form submission.
"use client"
import { produceNewMessage } from "./actions" ;
export default function FormExample () {
return (
< form
action = {produceNewMessage}
>
< label >
Name:
< input type = "text" name = "name" required />
</ label >
< button type = "submit" >Submit</ button >
</ form >
);
}
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.
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"
export const BasicForm = () => {
const { isPending , execute , isSuccess , data , isError , error } =
useServerAction (produceNewMessage)
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Form Example</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form
className = "flex flex-col gap-4"
onSubmit = { async ( event ) => {
event. preventDefault ()
const form = event.currentTarget
const formData = new FormData (form)
const [ data , err ] = await execute (formData)
if (err) {
// handle error
return
}
form. reset ()
}}
>
< Input type = "text" name = "name" placeholder = "Enter your name..." />
< Button className = "w-full" type = "submit" disabled = {isPending}>
Submit
</ Button >
{isPending && < div >Loading...</ div >}
{isSuccess && < div >Success: { JSON . stringify (data)}</ div >}
{isError && < div >Error: { JSON . stringify (error.fieldErrors)}</ div >}
</ form >
</ CardContent >
</ Card >
)
}
Here is the result:
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.
useServerAction
also supports executing a form action directly. This is useful when you want to handle form submissions in your React components.
To do this, you can use the executeFormAction
function passed back by useServerAction
hook.
First, make sure you have the input type set to "formData"
in your server action.
"use server"
import z from "zod"
import { createServerAction } from "zsa"
export const produceNewMessage = createServerAction ()
. input (
z. object ({
name: z. string (). min ( 5 ),
}),
{
type: "formData" ,
}
)
. handler ( async ({ input }) => {
await new Promise (( resolve ) => setTimeout (resolve, 500 ))
return "Hello, " + input.name
})
Then, you can use the executeFormAction
function to execute the form action.
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"
export const ExecuteFormAction = () => {
const { isPending , executeFormAction , isSuccess , data , isError , error } =
useServerAction (produceNewMessage)
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Form Example</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form className = "flex flex-col gap-4" action = {executeFormAction}>
< Input type = "text" name = "name" placeholder = "Enter your name..." />
< Button className = "w-full" type = "submit" disabled = {isPending}>
Submit
</ Button >
{isPending && < div >Loading...</ div >}
{isSuccess && < div >Success: { JSON . stringify (data)}</ div >}
{isError && < div >Error: { JSON . stringify (error.fieldErrors)}</ div >}
</ form >
</ CardContent >
</ Card >
)
}
Here is the result:
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:
"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.
"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:
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.
"use server"
import z from "zod"
import { createServerAction } from "zsa"
export const produceNewMessage = createServerAction ()
. input (
z. object ({
name: z. string (). min ( 5 ),
}),
{
type: "state" ,
}
)
. handler ( async ({ input }) => {
await new Promise (( resolve ) => setTimeout (resolve, 500 ))
return "Hello, " + input.name
})
Then on the client side, you can use the useActionState
hook to execute the action and handle the result.
"use client"
import { useActionState } from "react" ;
import { produceNewMessage } from "./actions" ;
export default function UseActionStateExample () {
let [[data, err], submitAction, isPending] = useActionState (
produceNewMessage,
[ null , null ] // or [initialData, null]
);
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Use Form State</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form action = {submitAction} className = "flex flex-col gap-4" >
< Input name = "name" placeholder = "Enter your name..." />
< Button disabled = {isPending}>Create message</ Button >
</ form >
{isPending && < div >Loading...</ div >}
{data && < p >Message: {data}</ p >}
{err && < div >Error: { JSON . stringify (err)}</ div >}
</ CardContent >
</ Card >
);
}
Here is the result:
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 control the state of the form yourself? Let's see how we can do that. This example will show how to append messages to an array.
First, make sure you are using formData
as the input type.
"use server"
import z from "zod"
import { createServerAction } from "zsa"
const produceNewMessageAction = createServerAction ()
. input (
z. object ({
name: z. string (),
}),
{
type: "formData" ,
}
)
. handler ( async ({ input }) => {
await new Promise (( resolve ) => setTimeout (resolve, 500 ))
return "Hello, " + input.name
})
export const produceNewMessage = async (
previousState : string [],
formData : FormData
) => {
const [ data , err ] = await produceNewMessageAction (formData)
if (err) {
// handle error
return previousState
}
return [ ... previousState, data]
}
Now we can use the useActionState
hook to control the state of the form.
use-action-state-custom-state.tsx "use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useActionState } from "react"
import { produceNewMessage } from "./actions"
export default function UseActionCustomStateExample () {
let [messages, submitAction] = useActionState (produceNewMessage, [
"my initial message" ,
])
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Use Action Custom State</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form action = {submitAction} className = "flex flex-col gap-4" >
< Input name = "name" placeholder = "Enter your name..." />
< Button >Create message</ Button >
</ form >
< h1 >Messages:</ h1 >
< div >
{messages. map (( message , index ) => (
< div key = {index}>{message}</ div >
))}
</ div >
</ CardContent >
</ Card >
)
}
Let's check out the result!
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.
"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.
use-action-state-skip-input-parsing.tsx "use client"
import { useActionState } from "react" ;
import { produceNewMessage } from "./actions" ;
export default function UseActionStateSkipInputParsingExample () {
let [[data, err], submitAction, isPending] = useActionState (
produceNewMessage,
[ null , null ] // or [initialData, null]
);
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Skip Input Parsing</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form action = {submitAction} className = "flex flex-col gap-4" >
< Input name = "name" placeholder = "Enter your name..." />
< Button disabled = {isPending}>Create message</ Button >
</ form >
{isPending && < div >Loading...</ div >}
{data && < p >Message: {data}</ p >}
{err && < div >Error: { JSON . stringify (err)}</ div >}
</ CardContent >
</ Card >
);
}
Here is 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.
"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:
"use client"
import { useActionState } from "zsa-react" ;
import { uploadFile } from "./actions" ;
export default function FileUploadExample () {
const [[ data , error ], submitAction , isPending ] = useActionState (uploadFile, [ null , null ]);
return (
< form action = {submitAction} className = "flex flex-col gap-4" >
< label >
Name:
< input type = "text" name = "name" required />
</ label >
< label >
File:
< input type = "file" name = "file" required />
</ label >
< button type = "submit" disabled = {isPending}>
Upload
</ button >
{isPending && < div >Uploading...</ div >}
{data && < div >Success: {data}</ div >}
{error && < div >Error: { JSON . stringify (error.fieldErrors)}</ div >}
</ 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:
"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:
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useActionState } from "react"
import { multiplyNumbersAction } from "./actions"
export default function MultiEntryExample () {
let [[data, err], submitAction, isPending] = useActionState (
multiplyNumbersAction,
[ null , null ]
)
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Multi Entry Form</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form action = {submitAction} className = "flex flex-col gap-4" >
< Input name = "number" placeholder = "Enter number..." type = "number" />
< Input name = "number" placeholder = "Enter number..." type = "number" />
< Input name = "filefield" type = "file" multiple />
< Button disabled = {isPending}>Multiply Numbers</ Button >
</ form >
{isPending && < div >Loading...</ div >}
{data && < p >Result: {data}</ p >}
{err && < div >Error: { JSON . stringify (err.fieldErrors)}</ div >}
</ CardContent >
</ Card >
)
}
Here is the result:
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.
"use server"
import z from "zod"
import { createServerAction } from "zsa"
export const produceNewMessage = createServerAction ()
. input (
z. object ({
name: z. string (). min ( 5 ),
otherArg1: z. string (),
}),
{
type: "formData" ,
}
)
. handler ( async ({ input }) => {
await new Promise (( resolve ) => setTimeout (resolve, 500 ))
return "Hello, " + input.name + " " + input.otherArg1
})
Then on the client side, you can use the useServerAction
hook to execute the server action and handle the result.
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useServerAction } from "zsa-react"
import { produceNewMessage } from "./actions"
export const BindFormExample = () => {
const { isPending , executeFormAction , isSuccess , data , isError , error } =
useServerAction (produceNewMessage, {
bind: {
otherArg1: "binded" ,
},
})
return (
< Card className = "not-prose" >
< CardHeader >
< CardTitle >Form Example</ CardTitle >
</ CardHeader >
< CardContent className = "flex flex-col gap-4" >
< form className = "flex flex-col gap-4" action = {executeFormAction}>
< Input type = "text" name = "name" placeholder = "Enter your name..." />
< Button className = "w-full" type = "submit" disabled = {isPending}>
Submit
</ Button >
{isPending && < div >Loading...</ div >}
{isSuccess && < div >Success: { JSON . stringify (data)}</ div >}
{isError && < div >Error: { JSON . stringify (error.fieldErrors)}</ div >}
</ form >
</ CardContent >
</ Card >
)
}
Here is the result:
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.
const { execute } = useServerAction (produceNewMessage)
return (
< form
onSubmit = { async ( event ) => {
event. preventDefault ()
const formData = new FormData (event.currentTarget)
const [ data , err ] = await execute (formData, {
otherArg1: "binded" ,
})
if (err) {
// handle error
return
}
}}
/>
)