|
| 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. |
0 commit comments