Skip to content

Commit 0842cff

Browse files
committed
chore: update ssr
1 parent cdf783c commit 0842cff

File tree

5 files changed

+504
-497
lines changed

5 files changed

+504
-497
lines changed

docs/config.json

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,16 @@
156156
"to": "framework/react/guides/react-native"
157157
},
158158
{
159-
"label": "SSR/TanStack Start/Next.js",
160-
"to": "framework/react/guides/ssr"
159+
"label": "TanStack Start",
160+
"to": "framework/react/guides/tanstack-start"
161+
},
162+
{
163+
"label": "Next.js",
164+
"to": "framework/react/guides/nextjs"
165+
},
166+
{
167+
"label": "Remix",
168+
"to": "framework/react/guides/remix"
161169
},
162170
{
163171
"label": "Debugging",
@@ -418,10 +426,6 @@
418426
"label": "Functions / useForm",
419427
"to": "framework/react/reference/functions/useForm"
420428
},
421-
{
422-
"label": "Functions / useTransform",
423-
"to": "framework/react/reference/functions/useTransform"
424-
},
425429
{
426430
"label": "Types / FieldComponent",
427431
"to": "framework/react/reference/type-aliases/FieldComponent"
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
id: ssr
3+
title: TanStack Form - NextJs
4+
---
5+
6+
## Using TanStack Form in a Next.js App Router
7+
8+
> Before reading this section, it's suggested you understand how React Server Components and React Server Actions work. [Check out this blog series for more information](https://playfulprogramming.com/collections/react-beyond-the-render)
9+
10+
This section focuses on integrating TanStack Form with `Next.js`, particularly using the `App Router` and `Server Actions`.
11+
12+
### Next.js Prerequisites
13+
14+
- Start a new `Next.js` project, following the steps in the [Next.js Documentation](https://nextjs.org/docs/getting-started/installation).
15+
- Install `@tanstack/react-form-nextjs`
16+
- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional]
17+
18+
## App Router integration
19+
20+
Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server.
21+
22+
```ts shared-code.ts
23+
import { formOptions } from '@tanstack/react-form-nextjs'
24+
25+
// You can pass other form options here
26+
export const formOpts = formOptions({
27+
defaultValues: {
28+
firstName: '',
29+
age: 0,
30+
},
31+
})
32+
```
33+
34+
Next, we can create [a React Server Action](https://playfulprogramming.com/posts/what-are-react-server-components) that will handle the form submission on the server.
35+
36+
```ts action.ts
37+
'use server'
38+
39+
import {
40+
ServerValidateError,
41+
createServerValidate,
42+
} from '@tanstack/react-form-nextjs'
43+
44+
import { formOpts } from './shared-code'
45+
46+
// Create the server action that will infer the types of the form from `formOpts`
47+
const serverValidate = createServerValidate({
48+
...formOpts,
49+
onServerValidate: ({ value }) => {
50+
if (value.age < 12) {
51+
return 'Server validation: You must be at least 12 to sign up'
52+
}
53+
},
54+
})
55+
56+
export default async function someAction(prev: unknown, formData: FormData) {
57+
try {
58+
const validatedData = await serverValidate(formData)
59+
console.log('validatedData', validatedData)
60+
// Persist the form data to the database
61+
// await sql`
62+
// INSERT INTO users (name, email, password)
63+
// VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password})
64+
// `
65+
} catch (e) {
66+
if (e instanceof ServerValidateError) {
67+
return e.formState
68+
}
69+
70+
// Some other error occurred while validating your form
71+
throw e
72+
}
73+
74+
// Your form has successfully validated!
75+
}
76+
```
77+
78+
Finally, we'll use `someAction` in our client-side form component.
79+
80+
```tsx client-component.tsx
81+
'use client'
82+
83+
import { useActionState } from 'react'
84+
import {
85+
initialFormState,
86+
mergeForm,
87+
useForm,
88+
useStore,
89+
useTransform,
90+
} from '@tanstack/react-form-nextjs'
91+
92+
import someAction from './action'
93+
import { formOpts } from './shared-code'
94+
95+
export const ClientComp = () => {
96+
const [state, action] = useActionState(someAction, initialFormState)
97+
98+
const form = useForm({
99+
...formOpts,
100+
transform: useTransform((baseForm) => mergeForm(baseForm, state!), [state]),
101+
})
102+
103+
const formErrors = useStore(form.store, (formState) => formState.errors)
104+
105+
return (
106+
<form action={action as never} onSubmit={() => form.handleSubmit()}>
107+
{formErrors.map((error) => (
108+
<p key={error as string}>{error}</p>
109+
))}
110+
111+
<form.Field
112+
name="age"
113+
validators={{
114+
onChange: ({ value }) =>
115+
value < 8 ? 'Client validation: You must be at least 8' : undefined,
116+
}}
117+
>
118+
{(field) => {
119+
return (
120+
<div>
121+
<input
122+
name={field.name} // must explicitly set the name attribute for the POST request
123+
type="number"
124+
value={field.state.value}
125+
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
126+
/>
127+
{field.state.meta.errors.map((error) => (
128+
<p key={error as string}>{error}</p>
129+
))}
130+
</div>
131+
)
132+
}}
133+
</form.Field>
134+
<form.Subscribe
135+
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
136+
>
137+
{([canSubmit, isSubmitting]) => (
138+
<button type="submit" disabled={!canSubmit}>
139+
{isSubmitting ? '...' : 'Submit'}
140+
</button>
141+
)}
142+
</form.Subscribe>
143+
</form>
144+
)
145+
}
146+
```
147+
148+
### useTransform
149+
150+
you may have noticed util function `useTransform` being used throughout these examples, it's primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state.
151+
152+
## debugging
153+
154+
> If you get the following error in your Next.js application:
155+
>
156+
> ```typescript
157+
> x You're importing a component that needs `useState`. This React hook only works in a client component. To fix, mark the file (or its parent) with the `"use client"` directive.
158+
> ```
159+
>
160+
> This is because you're not importing server-side code from `@tanstack/react-form-nextjs`. Ensure you're importing the correct module based on the environment.
161+
>
162+
> [This is a limitation of Next.js](https://github.com/phryneas/rehackt). Other meta-frameworks will likely not have this same problem.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
id: ssr
3+
title: TanStack Form - Remix
4+
---
5+
6+
## Using TanStack Form in Remix
7+
8+
> Before reading this section, it's suggested you understand how Remix actions work. [Check out Remix's docs for more information](https://remix.run/docs/en/main/discussion/data-flow#route-action)
9+
10+
### Remix Prerequisites
11+
12+
- Start a new `Remix` project, following the steps in the [Remix Documentation](https://remix.run/docs/en/main/start/quickstart).
13+
- Install `@tanstack/react-form`
14+
- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional]
15+
16+
## Remix integration
17+
18+
Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server.
19+
20+
```tsx routes/_index/route.tsx
21+
import { formOptions } from '@tanstack/react-form-remix'
22+
23+
// You can pass other form options here
24+
export const formOpts = formOptions({
25+
defaultValues: {
26+
firstName: '',
27+
age: 0,
28+
},
29+
})
30+
```
31+
32+
Next, we can create [an action](https://remix.run/docs/en/main/discussion/data-flow#route-action) that will handle the form submission on the server.
33+
34+
```tsx routes/_index/route.tsx
35+
import {
36+
ServerValidateError,
37+
createServerValidate,
38+
formOptions,
39+
} from '@tanstack/react-form-remix'
40+
41+
import type { ActionFunctionArgs } from '@remix-run/node'
42+
43+
// Create the server action that will infer the types of the form from `formOpts`
44+
const serverValidate = createServerValidate({
45+
...formOpts,
46+
onServerValidate: ({ value }) => {
47+
if (value.age < 12) {
48+
return 'Server validation: You must be at least 12 to sign up'
49+
}
50+
},
51+
})
52+
53+
export async function action({ request }: ActionFunctionArgs) {
54+
const formData = await request.formData()
55+
try {
56+
const validatedData = await serverValidate(formData)
57+
console.log('validatedData', validatedData)
58+
// Persist the form data to the database
59+
// await sql`
60+
// INSERT INTO users (name, email, password)
61+
// VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password})
62+
// `
63+
} catch (e) {
64+
if (e instanceof ServerValidateError) {
65+
return e.formState
66+
}
67+
68+
// Some other error occurred while validating your form
69+
throw e
70+
}
71+
72+
// Your form has successfully validated!
73+
}
74+
```
75+
76+
Finally, the `action` will be called when the form submits.
77+
78+
```tsx
79+
// routes/_index/route.tsx
80+
import { Form, useActionData } from '@remix-run/react'
81+
import { mergeForm, useForm, useStore } from '@tanstack/react-form'
82+
import {
83+
ServerValidateError,
84+
createServerValidate,
85+
formOptions,
86+
initialFormState,
87+
useTransform,
88+
} from '@tanstack/react-form-remix'
89+
90+
export default function Index() {
91+
const actionData = useActionData<typeof action>()
92+
93+
const form = useForm({
94+
...formOpts,
95+
transform: useTransform(
96+
(baseForm) => mergeForm(baseForm, actionData ?? initialFormState),
97+
[actionData],
98+
),
99+
})
100+
101+
const formErrors = useStore(form.store, (formState) => formState.errors)
102+
103+
return (
104+
<form method="post" onSubmit={() => form.handleSubmit()}>
105+
{formErrors.map((error) => (
106+
<p key={error as string}>{error}</p>
107+
))}
108+
109+
<form.Field
110+
name="age"
111+
validators={{
112+
onChange: ({ value }) =>
113+
value < 8 ? 'Client validation: You must be at least 8' : undefined,
114+
}}
115+
>
116+
{(field) => {
117+
return (
118+
<div>
119+
<input
120+
name="age"
121+
type="number"
122+
value={field.state.value}
123+
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
124+
/>
125+
{field.state.meta.errors.map((error) => (
126+
<p key={error as string}>{error}</p>
127+
))}
128+
</div>
129+
)
130+
}}
131+
</form.Field>
132+
<form.Subscribe
133+
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
134+
>
135+
{([canSubmit, isSubmitting]) => (
136+
<button type="submit" disabled={!canSubmit}>
137+
{isSubmitting ? '...' : 'Submit'}
138+
</button>
139+
)}
140+
</form.Subscribe>
141+
</form>
142+
)
143+
}
144+
```
145+
146+
### useTransform
147+
148+
you may have noticed util function `useTransform` being used throughout these examples, it's primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state.
149+
150+
```tsx
151+
const form = useForm({
152+
...formOpts,
153+
transform: useTransform(
154+
(baseForm) => mergeForm(baseForm, actionData ?? initialFormState),
155+
[actionData],
156+
),
157+
})
158+
```

0 commit comments

Comments
 (0)