or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced.mdfield-api.mdform-api.mdframework-integrations.mdhooks.mdindex.mdvalidation.md

framework-integrations.mddocs/

0

# Framework Integrations

1

2

Server-side validation utilities for Next.js, Remix, and TanStack Start frameworks. These integrations enable Progressive Enhancement patterns with server-side validation and client-side hydration.

3

4

## Capabilities

5

6

### Next.js Integration

7

8

Server-side validation for Next.js Server Actions.

9

10

```typescript { .api }

11

/**

12

* Creates a server validation function for Next.js Server Actions

13

* Validates FormData on the server and throws ServerValidateError on failure

14

*

15

* @param defaultOpts - Configuration for server validation

16

* @returns Async function that validates FormData and returns parsed data

17

*/

18

function createServerValidate<

19

TFormData,

20

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,

21

>(

22

defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,

23

): (

24

formData: FormData,

25

info?: { resolve?: (fieldName: string) => string | File },

26

) => Promise<TFormData>;

27

28

interface CreateServerValidateOptions<

29

TFormData,

30

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

31

> {

32

/** Default form values */

33

defaultValues?: TFormData;

34

35

/**

36

* Server-side validator function or Standard Schema

37

* Validates the parsed form data before processing

38

*/

39

onServerValidate?: TOnServer;

40

41

/**

42

* Custom parser for FormData

43

* @param formData - Raw FormData from form submission

44

* @returns Parsed form data object

45

*/

46

parse?: (formData: FormData) => TFormData;

47

}

48

49

/**

50

* Initial form state constant

51

* Use this as default state before server validation completes

52

*/

53

const initialFormState: ServerFormState<any, undefined>;

54

55

/**

56

* Server validation error class

57

* Thrown by createServerValidate when validation fails

58

*/

59

class ServerValidateError<

60

TFormData,

61

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

62

> extends Error {

63

/** Form state with validation errors */

64

formState: ServerFormState<TFormData, TOnServer>;

65

66

constructor(formState: ServerFormState<TFormData, TOnServer>);

67

}

68

69

/**

70

* Server form state type

71

* Subset of full FormState used for server-side validation

72

*/

73

type ServerFormState<

74

TFormData,

75

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

76

> = Pick<

77

FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,

78

'values' | 'errors' | 'errorMap'

79

>;

80

```

81

82

**Usage Example:**

83

84

```typescript

85

// app/actions.ts (Next.js Server Action)

86

'use server';

87

88

import { createServerValidate } from '@tanstack/react-form/nextjs';

89

import { z } from 'zod';

90

91

const serverValidate = createServerValidate({

92

defaultValues: {

93

name: '',

94

email: '',

95

},

96

onServerValidate: z.object({

97

name: z.string().min(2),

98

email: z.string().email(),

99

}),

100

});

101

102

export async function submitForm(prev: any, formData: FormData) {

103

try {

104

const data = await serverValidate(formData);

105

106

// Process validated data

107

await saveToDatabase(data);

108

109

return { success: true };

110

} catch (error) {

111

if (error instanceof ServerValidateError) {

112

return error.formState;

113

}

114

throw error;

115

}

116

}

117

118

// app/form.tsx (Client Component)

119

'use client';

120

121

import { useForm } from '@tanstack/react-form';

122

import { useFormState } from 'react-dom';

123

import { submitForm } from './actions';

124

import { initialFormState } from '@tanstack/react-form/nextjs';

125

126

export function MyForm() {

127

const [serverFormState, formAction] = useFormState(submitForm, initialFormState);

128

129

const form = useForm({

130

defaultValues: {

131

name: '',

132

email: '',

133

},

134

// Merge server validation errors with client state

135

defaultState: serverFormState,

136

});

137

138

return (

139

<form action={formAction}>

140

<form.Field name="name">

141

{(field) => (

142

<input

143

name={field.name}

144

value={field.state.value}

145

onChange={(e) => field.handleChange(e.target.value)}

146

/>

147

)}

148

</form.Field>

149

150

<form.Field name="email">

151

{(field) => (

152

<input

153

name={field.name}

154

value={field.state.value}

155

onChange={(e) => field.handleChange(e.target.value)}

156

/>

157

)}

158

</form.Field>

159

160

<button type="submit">Submit</button>

161

</form>

162

);

163

}

164

```

165

166

### Remix Integration

167

168

Server-side validation for Remix Form Actions.

169

170

```typescript { .api }

171

/**

172

* Creates a server validation function for Remix actions

173

* Validates FormData on the server and throws ServerValidateError on failure

174

*

175

* @param defaultOpts - Configuration for server validation

176

* @returns Async function that validates FormData and returns parsed data

177

*/

178

function createServerValidate<

179

TFormData,

180

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,

181

>(

182

defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,

183

): (

184

formData: FormData,

185

info?: { resolve?: (fieldName: string) => string | File },

186

) => Promise<TFormData>;

187

188

/**

189

* Initial form state constant

190

* Use this as default state in loader functions

191

*/

192

const initialFormState: ServerFormState<any, undefined>;

193

194

/**

195

* Server validation error class

196

* Thrown by createServerValidate when validation fails

197

*/

198

class ServerValidateError<

199

TFormData,

200

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

201

> extends Error {

202

/** Form state with validation errors */

203

formState: ServerFormState<TFormData, TOnServer>;

204

205

constructor(formState: ServerFormState<TFormData, TOnServer>);

206

}

207

208

/** Server form state type (same as Next.js) */

209

type ServerFormState<

210

TFormData,

211

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

212

> = Pick<

213

FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,

214

'values' | 'errors' | 'errorMap'

215

>;

216

```

217

218

**Usage Example:**

219

220

```typescript

221

// app/routes/contact.tsx (Remix)

222

import { json, type ActionFunctionArgs } from '@remix-run/node';

223

import { useActionData, Form } from '@remix-run/react';

224

import { useForm } from '@tanstack/react-form';

225

import { createServerValidate, initialFormState } from '@tanstack/react-form/remix';

226

import { z } from 'zod';

227

228

const serverValidate = createServerValidate({

229

defaultValues: {

230

name: '',

231

email: '',

232

message: '',

233

},

234

onServerValidate: z.object({

235

name: z.string().min(2),

236

email: z.string().email(),

237

message: z.string().min(10),

238

}),

239

});

240

241

export async function action({ request }: ActionFunctionArgs) {

242

const formData = await request.formData();

243

244

try {

245

const data = await serverValidate(formData);

246

247

// Process validated data

248

await sendEmail(data);

249

250

return json({ success: true });

251

} catch (error) {

252

if (error instanceof ServerValidateError) {

253

return json(error.formState);

254

}

255

throw error;

256

}

257

}

258

259

export default function ContactRoute() {

260

const actionData = useActionData<typeof action>();

261

const serverFormState = actionData?.success ? initialFormState : actionData;

262

263

const form = useForm({

264

defaultValues: {

265

name: '',

266

email: '',

267

message: '',

268

},

269

defaultState: serverFormState,

270

});

271

272

return (

273

<Form method="post">

274

<form.Field name="name">

275

{(field) => (

276

<div>

277

<input

278

name={field.name}

279

value={field.state.value}

280

onChange={(e) => field.handleChange(e.target.value)}

281

/>

282

{field.state.meta.errors[0]}

283

</div>

284

)}

285

</form.Field>

286

287

<form.Field name="email">

288

{(field) => (

289

<div>

290

<input

291

name={field.name}

292

value={field.state.value}

293

onChange={(e) => field.handleChange(e.target.value)}

294

/>

295

{field.state.meta.errors[0]}

296

</div>

297

)}

298

</form.Field>

299

300

<form.Field name="message">

301

{(field) => (

302

<div>

303

<textarea

304

name={field.name}

305

value={field.state.value}

306

onChange={(e) => field.handleChange(e.target.value)}

307

/>

308

{field.state.meta.errors[0]}

309

</div>

310

)}

311

</form.Field>

312

313

<button type="submit">Submit</button>

314

</Form>

315

);

316

}

317

```

318

319

### TanStack Start Integration

320

321

Server-side validation for TanStack Start with cookie-based state persistence.

322

323

```typescript { .api }

324

/**

325

* Creates a server validation function for TanStack Start

326

* Validates FormData and stores state in cookies for post-redirect access

327

*

328

* @param defaultOpts - Configuration for server validation

329

* @returns Async function that validates FormData and returns parsed data

330

*/

331

function createServerValidate<

332

TFormData,

333

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,

334

>(

335

defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,

336

): (

337

formData: FormData,

338

info?: { resolve?: (fieldName: string) => string | File },

339

) => Promise<TFormData>;

340

341

/**

342

* Retrieves form data from cookies set by server validation

343

* Use this in your component to access validation state after redirect

344

*

345

* @returns Promise resolving to server form state or initialFormState

346

*/

347

function getFormData(): Promise<

348

ServerFormState<any, undefined> | typeof initialFormState

349

>;

350

351

/**

352

* Initial form state constant

353

*/

354

const initialFormState: {

355

errorMap: { onServer: undefined };

356

errors: [];

357

};

358

359

/**

360

* Server validation error class for TanStack Start

361

* Includes Response object for redirect handling

362

*/

363

class ServerValidateError<

364

TFormData,

365

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

366

> extends Error {

367

/** Form state with validation errors */

368

formState: ServerFormState<TFormData, TOnServer>;

369

370

/** Response object for redirects and headers */

371

response: Response;

372

373

constructor(formState: ServerFormState<TFormData, TOnServer>, response: Response);

374

}

375

376

/** Server form state type */

377

type ServerFormState<

378

TFormData,

379

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

380

> = Pick<

381

FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,

382

'values' | 'errors' | 'errorMap'

383

>;

384

```

385

386

**Usage Example:**

387

388

```typescript

389

// app/routes/contact.tsx (TanStack Start)

390

import { createServerFn } from '@tanstack/start';

391

import { useServerFn } from '@tanstack/start';

392

import { useForm } from '@tanstack/react-form';

393

import {

394

createServerValidate,

395

getFormData,

396

initialFormState,

397

ServerValidateError,

398

} from '@tanstack/react-form/start';

399

import { z } from 'zod';

400

401

const serverValidate = createServerValidate({

402

defaultValues: {

403

name: '',

404

email: '',

405

},

406

onServerValidate: z.object({

407

name: z.string().min(2),

408

email: z.string().email(),

409

}),

410

});

411

412

const submitFormAction = createServerFn({ method: 'POST' })

413

.validator((formData: FormData) => formData)

414

.handler(async ({ data }) => {

415

try {

416

const validated = await serverValidate(data);

417

418

// Process validated data

419

await saveToDatabase(validated);

420

421

return { success: true };

422

} catch (error) {

423

if (error instanceof ServerValidateError) {

424

// Throw the response with cookies

425

throw error.response;

426

}

427

throw error;

428

}

429

});

430

431

export default function ContactRoute() {

432

const serverFormState = getFormData();

433

const submitForm = useServerFn(submitFormAction);

434

435

const form = useForm({

436

defaultValues: {

437

name: '',

438

email: '',

439

},

440

defaultState: serverFormState,

441

onSubmit: async ({ value }) => {

442

const formData = new FormData();

443

formData.append('name', value.name);

444

formData.append('email', value.email);

445

446

await submitForm({ data: formData });

447

},

448

});

449

450

return (

451

<form

452

onSubmit={(e) => {

453

e.preventDefault();

454

form.handleSubmit();

455

}}

456

>

457

<form.Field name="name">

458

{(field) => (

459

<div>

460

<input

461

value={field.state.value}

462

onChange={(e) => field.handleChange(e.target.value)}

463

/>

464

{field.state.meta.errors[0]}

465

</div>

466

)}

467

</form.Field>

468

469

<form.Field name="email">

470

{(field) => (

471

<div>

472

<input

473

value={field.state.value}

474

onChange={(e) => field.handleChange(e.target.value)}

475

/>

476

{field.state.meta.errors[0]}

477

</div>

478

)}

479

</form.Field>

480

481

<button type="submit">Submit</button>

482

</form>

483

);

484

}

485

```

486

487

## Import Paths

488

489

```typescript

490

// Next.js

491

import {

492

createServerValidate,

493

initialFormState,

494

ServerValidateError,

495

type ServerFormState,

496

} from '@tanstack/react-form/nextjs';

497

498

// Remix

499

import {

500

createServerValidate,

501

initialFormState,

502

ServerValidateError,

503

type ServerFormState,

504

} from '@tanstack/react-form/remix';

505

506

// TanStack Start

507

import {

508

createServerValidate,

509

getFormData,

510

initialFormState,

511

ServerValidateError,

512

type ServerFormState,

513

} from '@tanstack/react-form/start';

514

```

515

516

## Common Patterns

517

518

### Progressive Enhancement

519

520

All framework integrations support Progressive Enhancement where forms work without JavaScript:

521

522

```typescript

523

// The form submits to server even without JS

524

<form action={formAction} method="post">

525

{/* name attribute required for server-side parsing */}

526

<input name="email" />

527

<button type="submit">Submit</button>

528

</form>

529

530

// With JS, client-side validation enhances UX

531

<form

532

action={formAction}

533

onSubmit={(e) => {

534

e.preventDefault();

535

form.handleSubmit();

536

}}

537

>

538

<form.Field name="email">

539

{(field) => (

540

<input

541

name={field.name}

542

value={field.state.value}

543

onChange={(e) => field.handleChange(e.target.value)}

544

/>

545

)}

546

</form.Field>

547

</form>

548

```

549

550

### Custom FormData Parsing

551

552

```typescript

553

const serverValidate = createServerValidate({

554

defaultValues: {

555

tags: [],

556

metadata: {},

557

},

558

parse: (formData) => {

559

return {

560

tags: formData.getAll('tags'),

561

metadata: JSON.parse(formData.get('metadata') as string),

562

};

563

},

564

onServerValidate: (data) => {

565

if (data.tags.length === 0) {

566

return 'At least one tag required';

567

}

568

return undefined;

569

},

570

});

571

```

572

573

### File Upload Handling

574

575

```typescript

576

const serverValidate = createServerValidate({

577

defaultValues: {

578

file: null as File | null,

579

},

580

parse: (formData) => ({

581

file: formData.get('file') as File,

582

}),

583

onServerValidate: async ({ value }) => {

584

if (!value.file) {

585

return { fields: { file: 'File is required' } };

586

}

587

588

if (value.file.size > 5 * 1024 * 1024) {

589

return { fields: { file: 'File must be less than 5MB' } };

590

}

591

592

return undefined;

593

},

594

});

595

```

596

597

### Shared Validation Between Client and Server

598

599

```typescript

600

// shared/validation.ts

601

import { z } from 'zod';

602

603

export const contactSchema = z.object({

604

name: z.string().min(2, 'Name must be at least 2 characters'),

605

email: z.string().email('Invalid email address'),

606

message: z.string().min(10, 'Message must be at least 10 characters'),

607

});

608

609

// server/action.ts

610

import { createServerValidate } from '@tanstack/react-form/nextjs';

611

import { contactSchema } from '../shared/validation';

612

613

export const serverValidate = createServerValidate({

614

defaultValues: {

615

name: '',

616

email: '',

617

message: '',

618

},

619

onServerValidate: contactSchema,

620

});

621

622

// client/form.tsx

623

import { useForm } from '@tanstack/react-form';

624

import { contactSchema } from '../shared/validation';

625

626

export function ContactForm() {

627

const form = useForm({

628

defaultValues: {

629

name: '',

630

email: '',

631

message: '',

632

},

633

validators: {

634

onChange: contactSchema,

635

},

636

});

637

638

return <form>{/* fields */}</form>;

639

}

640

```

641