T
TanStack6mo ago
rival-black

Next js server actions with tanstack form and zod

I've been looking at the tanstack form validation with the zod schema. I've followed the documentation and made a client side component with zod and everything works great. Now i've been looking at the Next Server Actions example https://tanstack.com/form/latest/docs/framework/react/examples/next-server-actions?path=examples%2Freact%2Fnext-server-actions%2Fsrc%2Fapp%2Fclient-component.tsx And if I try that it works fine as well, however when I try to add the zodSchema validator the formErrors that were previously undefined now give a Record<string, StandardSchemaV1Issue[]> | undefined)[] from zod. It is unclear to me how I now would throw/show the right errors in this case. And is it possible in the action.ts to use the zod schema as well instead of using onServerValidate and then throw custom errors in case someone somehow passed the client side validation?
React TanStack Form Next Server Actions Example | TanStack Form Docs
An example showing how to implement Next Server Actions in React using TanStack Form.
5 Replies
conscious-sapphire
conscious-sapphire6mo ago
in the client component there is this mergeForm thing which should take the errors returned by teh server action and integrate it into the form. It is working for me, isn't this working for you?
No description
conscious-sapphire
conscious-sapphire6mo ago
it should then display errors here
No description
conscious-sapphire
conscious-sapphire6mo ago
and this is where the actual form errors are returned from the server side
No description
rival-black
rival-blackOP6mo ago
Yeah i'm aware but i meant if i add the validators: { onChange: zodSchema } in the useform below the transform part then that <p key={error as unknown as string}>{error}</p> doesn't work anymore it will throw errors once you try to type something So this
'use client'

import { useActionState } from 'react'
import { mergeForm, useForm, useTransform } from '@tanstack/react-form'
import { initialFormState } from '@tanstack/react-form/nextjs'
import { useStore } from '@tanstack/react-store'
import someAction from "@/app/action";
import {clientSchema, formOpts} from "@/schemas/clientSchema";
import {FieldInfo} from "@/component/fieldInfo";

export const ServerClientComponent = () => {
const [state, action] = useActionState(someAction, initialFormState)

const form = useForm({
...formOpts,
transform: useTransform(
(baseForm) => mergeForm(baseForm, state ?? {}),
[state],
),
onSubmit: ({value}) => {
alert(JSON.stringify(value, null, 2))
},
validators: {
onChange: clientSchema
}
})

const formErrors = useStore(form.store, (formState) => formState.errors)

return (
<form action={action as never} onSubmit={() => form.handleSubmit()}>
{formErrors.map((error) => (
<p key={error as unknown as string}>{error}</p>
))}
)
}
'use client'

import { useActionState } from 'react'
import { mergeForm, useForm, useTransform } from '@tanstack/react-form'
import { initialFormState } from '@tanstack/react-form/nextjs'
import { useStore } from '@tanstack/react-store'
import someAction from "@/app/action";
import {clientSchema, formOpts} from "@/schemas/clientSchema";
import {FieldInfo} from "@/component/fieldInfo";

export const ServerClientComponent = () => {
const [state, action] = useActionState(someAction, initialFormState)

const form = useForm({
...formOpts,
transform: useTransform(
(baseForm) => mergeForm(baseForm, state ?? {}),
[state],
),
onSubmit: ({value}) => {
alert(JSON.stringify(value, null, 2))
},
validators: {
onChange: clientSchema
}
})

const formErrors = useStore(form.store, (formState) => formState.errors)

return (
<form action={action as never} onSubmit={() => form.handleSubmit()}>
{formErrors.map((error) => (
<p key={error as unknown as string}>{error}</p>
))}
)
}
does not work (ignore that it doesn't show the field and buttons the text was too long
<form.Field name="name">
{(field) => (
<>
<label htmlFor={field.name}>Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="string"
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field}/>
</>
)}
</form.Field>

<form.Field
name="age"
>
{(field) => {
return (
<div>
<label htmlFor={field.name}>Age:</label>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
<FieldInfo field={field}/>
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
<form.Field name="name">
{(field) => (
<>
<label htmlFor={field.name}>Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="string"
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field}/>
</>
)}
</form.Field>

<form.Field
name="age"
>
{(field) => {
return (
<div>
<label htmlFor={field.name}>Age:</label>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
<FieldInfo field={field}/>
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
that would be the rest
compatible-crimson
compatible-crimson5mo ago
we're you able to resolve this @Espy ?

Did you find this page helpful?