11---
2- title : " Form v2 is here: All you need to know about the alpha"
2+ title : ' Form v2 is here: All you need to know about the alpha'
33published : 2026-08-06
44excerpt : The TanStack Form v2 alpha is here with more flexible validators, schema-oriented forms, safer form composition, and simpler SSR.
55library : form
@@ -29,12 +29,12 @@ Because v1 registered the validator once for each event, it could produce duplic
2929
3030``` ts title="v1"
3131const form = useForm ({
32- defaultValues: { name: " " },
32+ defaultValues: { name: ' ' },
3333 validators: {
3434 onChange: mySchema ,
3535 onBlur: mySchema ,
3636 },
37- });
37+ })
3838```
3939
4040#### After (v2)
@@ -43,14 +43,14 @@ In v2, you define the validator once and list both events in `triggers`. It can
4343
4444``` ts title="v2"
4545const form = useForm ({
46- defaultValues: { name: " " },
46+ defaultValues: { name: ' ' },
4747 validators: [
4848 {
4949 run: mySchema ,
50- triggers: [" change" , " blur" ],
50+ triggers: [' change' , ' blur' ],
5151 },
5252 ],
53- });
53+ })
5454```
5555
5656### Multiple validators, one trigger
@@ -61,18 +61,18 @@ The opposite was awkward too. Say you want to run both a schema validator and a
6161
6262``` ts title="v1"
6363const form = useForm ({
64- defaultValues: { name: " " },
64+ defaultValues: { name: ' ' },
6565 validators: {
6666 onChange : ({ formApi , value }) => {
67- const errors = formApi .parseValuesWithSchema (mySchema );
67+ const errors = formApi .parseValuesWithSchema (mySchema )
6868
6969 // Stop if the schema validator found any errors.
70- if (errors ) return errors ;
70+ if (errors ) return errors
7171
72- return checkReservedUsername (value );
72+ return checkReservedUsername (value )
7373 },
7474 },
75- });
75+ })
7676```
7777
7878#### After (v2)
@@ -81,20 +81,20 @@ In v2, the two validators stay separate even though they share a trigger. Settin
8181
8282``` ts title="v2"
8383const form = useForm ({
84- defaultValues: { name: " " },
84+ defaultValues: { name: ' ' },
8585 validators: [
8686 {
8787 run: mySchema ,
88- triggers: [" change" ],
88+ triggers: [' change' ],
8989 },
9090 {
9191 run : ({ value }) => checkReservedUsername (value ),
92- triggers: [" change" ],
92+ triggers: [' change' ],
9393 // Only run this check if the schema validator passes.
9494 bailIfInvalid: true ,
9595 },
9696 ],
97- });
97+ })
9898```
9999
100100### Conditional validators
@@ -107,12 +107,12 @@ In v1, general conditions had to live inside the validator. The function still r
107107
108108``` ts title="v1"
109109const form = useForm ({
110- defaultValues: { name: " " },
110+ defaultValues: { name: ' ' },
111111 validationLogic: revalidateLogic (),
112112 validators: {
113113 onDynamic: mySchema ,
114114 },
115- });
115+ })
116116```
117117
118118#### After (v2)
@@ -121,20 +121,20 @@ In v2, each trigger can include a `when` condition. The validator still runs on
121121
122122``` ts title="v2"
123123const form = useForm ({
124- defaultValues: { name: " " },
124+ defaultValues: { name: ' ' },
125125 validators: [
126126 {
127127 run: schema ,
128128 triggers: [
129129 {
130- trigger: " change" ,
130+ trigger: ' change' ,
131131 // After the first submission attempt, validate every change.
132132 when : ({ formApi }) => formApi .state .submissionAttempts > 0 ,
133133 },
134134 ],
135135 },
136136 ],
137- });
137+ })
138138```
139139
140140## Listeners rework
@@ -152,7 +152,7 @@ Consider an appointment form. Its schema requires a date, but we don't want to p
152152``` ts
153153const schema = z .object ({
154154 appointment: z .date (),
155- });
155+ })
156156
157157/*
158158 z.input<typeof schema> = {
@@ -176,10 +176,10 @@ const formOpts = formOptions({
176176 {
177177 // Error: The form is `appointment: null`, but the schema expects `Date`.
178178 run: schema ,
179- triggers: [" change" ],
179+ triggers: [' change' ],
180180 },
181181 ],
182- });
182+ })
183183```
184184
185185``` ts title="Strict schema"
@@ -191,10 +191,10 @@ const formOpts = formOptions.strictSchema({
191191 validators: [
192192 {
193193 run: schema ,
194- triggers: [" change" ],
194+ triggers: [' change' ],
195195 },
196196 ],
197- });
197+ })
198198```
199199
200200``` ts title="Loose schema"
@@ -206,10 +206,10 @@ const formOpts = formOptions.looseSchema({
206206 validators: [
207207 {
208208 run: schema ,
209- triggers: [" change" ],
209+ triggers: [' change' ],
210210 },
211211 ],
212- });
212+ })
213213```
214214
215215<!-- ::end:tabs -->
@@ -263,69 +263,69 @@ v1 configured server validation separately from the shared form options. Validat
263263<!-- ::start:tabs variant="files" -->
264264
265265``` ts title="shared-code.ts"
266- import { formOptions } from " @tanstack/react-form-nextjs" ;
266+ import { formOptions } from ' @tanstack/react-form-nextjs'
267267
268268export const formOpts = formOptions ({
269269 defaultValues: { age: 0 },
270- });
270+ })
271271```
272272
273273``` ts title="action.ts"
274- " use server" ;
274+ ' use server'
275275
276276import {
277277 createServerValidate ,
278278 ServerValidateError ,
279- } from " @tanstack/react-form-nextjs" ;
280- import { formOpts } from " ./shared-code" ;
279+ } from ' @tanstack/react-form-nextjs'
280+ import { formOpts } from ' ./shared-code'
281281
282282const mySchema = z .object ({
283- age: z .coerce .number ().min (13 , " You must be 13 at least 13" ),
284- });
283+ age: z .coerce .number ().min (13 , ' You must be 13 at least 13' ),
284+ })
285285
286286const serverValidate = createServerValidate ({
287287 ... formOpts ,
288288 onServerValidate: mySchema ,
289- });
289+ })
290290
291291export async function submit(_previous : unknown , formData : FormData ) {
292292 try {
293- const values = await serverValidate (formData );
293+ const values = await serverValidate (formData )
294294
295295 // Use values...
296296 } catch (error ) {
297297 // The returned form state loses its inferred type after this check.
298298 if (error instanceof ServerValidateError ) {
299- return error .formState ;
299+ return error .formState
300300 }
301301
302- throw error ;
302+ throw error
303303 }
304304}
305305```
306306
307307``` tsx title="client.tsx"
308- " use client" ;
308+ ' use client'
309309
310- import { useActionState } from " react" ;
310+ import { useActionState } from ' react'
311311import {
312312 initialFormState ,
313313 mergeForm ,
314314 useForm ,
315315 useTransform ,
316- } from " @tanstack/react-form-nextjs" ;
317- import { submit } from " ./action" ;
318- import { formOpts } from " ./shared-code" ;
316+ } from ' @tanstack/react-form-nextjs'
317+ import { submit } from ' ./action'
318+ import { formOpts } from ' ./shared-code'
319319
320320export function Form() {
321- const [state, action] = useActionState (submit , initialFormState );
321+ const [state, action] = useActionState (submit , initialFormState )
322322
323323 const form = useForm ({
324324 ... formOpts ,
325325 transform: useTransform ((baseForm ) => mergeForm (baseForm , state ! ), [state ]),
326- });
326+ })
327327
328- return <form action = { action as never } >{ /* form.Field components */ } </form >;
328+ return <form action = { action as never } >{ /* form.Field components */ } </form >
329329}
330330```
331331
@@ -338,68 +338,68 @@ v2 moves the server validator into the shared `formOpts`, so the same configurat
338338<!-- ::start:tabs variant="files" -->
339339
340340``` ts title="shared-code.ts"
341- import { formOptions } from " @tanstack/react-form" ;
341+ import { formOptions } from ' @tanstack/react-form'
342342
343343const mySchema = z .object ({
344- age: z .coerce .number ().min (13 , " You must be 13 at least 13" ),
345- });
344+ age: z .coerce .number ().min (13 , ' You must be 13 at least 13' ),
345+ })
346346
347347export const formOpts = formOptions ({
348348 defaultValues: { age: 0 },
349349 validators: [
350350 {
351- triggers: [" server" ],
351+ triggers: [' server' ],
352352 runOnSubmit: false ,
353353 run: mySchema ,
354354 },
355355 ],
356- });
356+ })
357357```
358358
359359``` ts title="action.ts"
360- " use server" ;
360+ ' use server'
361361
362362import {
363363 initialServerFormState ,
364364 serverValidateHelper ,
365- } from " @tanstack/react-form" ;
366- import { next } from " @tanstack/react-form-nextjs" ;
367- import { formOpts } from " ./shared-code" ;
365+ } from ' @tanstack/react-form'
366+ import { next } from ' @tanstack/react-form-nextjs'
367+ import { formOpts } from ' ./shared-code'
368368
369369const { createServerValidate } = serverValidateHelper ({
370370 framework: next (),
371- });
371+ })
372372
373- const serverValidate = createServerValidate (formOpts );
373+ const serverValidate = createServerValidate (formOpts )
374374
375375export async function submit(_previous : unknown , formData : FormData ) {
376- const result = await serverValidate (formData );
376+ const result = await serverValidate (formData )
377377
378378 // serverState keeps the type inferred from formOpts.
379- if (! result .success ) return result .serverState ;
379+ if (! result .success ) return result .serverState
380380
381381 // Use result.values...
382- return initialServerFormState ;
382+ return initialServerFormState
383383}
384384```
385385
386386``` tsx title="client.tsx"
387- " use client" ;
387+ ' use client'
388388
389- import { useActionState } from " react" ;
390- import { initialServerFormState , useForm } from " @tanstack/react-form" ;
391- import { submit } from " ./action" ;
392- import { formOpts } from " ./shared-code" ;
389+ import { useActionState } from ' react'
390+ import { initialServerFormState , useForm } from ' @tanstack/react-form'
391+ import { submit } from ' ./action'
392+ import { formOpts } from ' ./shared-code'
393393
394394export function Form() {
395- const [serverState, action] = useActionState (submit , initialServerFormState );
395+ const [serverState, action] = useActionState (submit , initialServerFormState )
396396
397397 const form = useForm ({
398398 ... formOpts ,
399399 serverState ,
400- });
400+ })
401401
402- return <form action = { action } >{ /* form.Field components */ } </form >;
402+ return <form action = { action } >{ /* form.Field components */ } </form >
403403}
404404```
405405
0 commit comments