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
4- excerpt : TanStack Form v2
4+ excerpt : The TanStack Form v2 alpha is here with more flexible validators, schema-oriented forms, safer form composition, and simpler SSR.
55library : form
66authors :
77 - Luca Jakob
@@ -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