-
-
Notifications
You must be signed in to change notification settings - Fork 622
docs: general docs updates #2118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
f9616f9
cdf783c
0842cff
707b5ed
aff13ab
efa1fe7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,10 @@ Finally, you can use a subfield like so: | |
| </form.Field> | ||
| ``` | ||
|
|
||
| ## Why Index as Key? | ||
|
|
||
| You may notice that these examples use `key={i}` the array index as the key prop. React's documentation generally advises _against_ using array indices as keys when items can be reordered or deleted. TanStack Form is an exception to this rule. Because field names in TanStack Form arrays are index-based, using the array index as `key` is required, it keeps React component instances, form store state, and field names in sync. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only partially true. Yes, the field data is identified by the index, but the rendering usually isn't. Many reordering libraries break when using the index as key. We addressed that for React, so while it may impact performance slightly, you can use arbitrary key props. I believe it's still broken in Vue.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah when was that changed? |
||
|
|
||
| ## Full Example | ||
|
|
||
| ```jsx | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,162 @@ | ||||||
| --- | ||||||
| id: ssr | ||||||
| title: TanStack Form - NextJs | ||||||
| --- | ||||||
|
|
||||||
| ## Using TanStack Form in a Next.js App Router | ||||||
|
|
||||||
| > 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) | ||||||
|
|
||||||
| This section focuses on integrating TanStack Form with `Next.js`, particularly using the `App Router` and `Server Actions`. | ||||||
|
|
||||||
| ### Next.js Prerequisites | ||||||
|
|
||||||
| - Start a new `Next.js` project, following the steps in the [Next.js Documentation](https://nextjs.org/docs/getting-started/installation). | ||||||
| - Install `@tanstack/react-form-nextjs` | ||||||
| - Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] | ||||||
|
|
||||||
| ## App Router integration | ||||||
|
|
||||||
| Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. | ||||||
|
|
||||||
| ```ts shared-code.ts | ||||||
| import { formOptions } from '@tanstack/react-form-nextjs' | ||||||
|
|
||||||
| // You can pass other form options here | ||||||
| export const formOpts = formOptions({ | ||||||
| defaultValues: { | ||||||
| firstName: '', | ||||||
| age: 0, | ||||||
| }, | ||||||
| }) | ||||||
| ``` | ||||||
|
|
||||||
| 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. | ||||||
|
|
||||||
| ```ts action.ts | ||||||
| 'use server' | ||||||
|
|
||||||
| import { | ||||||
| ServerValidateError, | ||||||
| createServerValidate, | ||||||
| } from '@tanstack/react-form-nextjs' | ||||||
|
|
||||||
| import { formOpts } from './shared-code' | ||||||
|
|
||||||
| // Create the server action that will infer the types of the form from `formOpts` | ||||||
| const serverValidate = createServerValidate({ | ||||||
| ...formOpts, | ||||||
| onServerValidate: ({ value }) => { | ||||||
| if (value.age < 12) { | ||||||
| return 'Server validation: You must be at least 12 to sign up' | ||||||
| } | ||||||
| }, | ||||||
| }) | ||||||
|
|
||||||
| export default async function someAction(prev: unknown, formData: FormData) { | ||||||
| try { | ||||||
| const validatedData = await serverValidate(formData) | ||||||
| console.log('validatedData', validatedData) | ||||||
| // Persist the form data to the database | ||||||
| // await sql` | ||||||
| // INSERT INTO users (name, email, password) | ||||||
| // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) | ||||||
| // ` | ||||||
| } catch (e) { | ||||||
| if (e instanceof ServerValidateError) { | ||||||
| return e.formState | ||||||
| } | ||||||
|
|
||||||
| // Some other error occurred while validating your form | ||||||
| throw e | ||||||
| } | ||||||
|
|
||||||
| // Your form has successfully validated! | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Finally, we'll use `someAction` in our client-side form component. | ||||||
|
|
||||||
| ```tsx client-component.tsx | ||||||
| 'use client' | ||||||
|
|
||||||
| import { useActionState } from 'react' | ||||||
| import { | ||||||
| initialFormState, | ||||||
| mergeForm, | ||||||
| useForm, | ||||||
| useStore, | ||||||
| useTransform, | ||||||
| } from '@tanstack/react-form-nextjs' | ||||||
|
|
||||||
| import someAction from './action' | ||||||
| import { formOpts } from './shared-code' | ||||||
|
|
||||||
| export const ClientComp = () => { | ||||||
| const [state, action] = useActionState(someAction, initialFormState) | ||||||
|
|
||||||
| const form = useForm({ | ||||||
| ...formOpts, | ||||||
| transform: useTransform((baseForm) => mergeForm(baseForm, state!), [state]), | ||||||
| }) | ||||||
|
|
||||||
| const formErrors = useStore(form.store, (formState) => formState.errors) | ||||||
|
|
||||||
| return ( | ||||||
| <form action={action as never} onSubmit={() => form.handleSubmit()}> | ||||||
| {formErrors.map((error) => ( | ||||||
| <p key={error as string}>{error}</p> | ||||||
| ))} | ||||||
|
|
||||||
| <form.Field | ||||||
| name="age" | ||||||
| validators={{ | ||||||
| onChange: ({ value }) => | ||||||
| value < 8 ? 'Client validation: You must be at least 8' : undefined, | ||||||
| }} | ||||||
| > | ||||||
| {(field) => { | ||||||
| return ( | ||||||
| <div> | ||||||
| <input | ||||||
| name={field.name} // must explicitly set the name attribute for the POST request | ||||||
| type="number" | ||||||
| value={field.state.value} | ||||||
| onChange={(e) => field.handleChange(e.target.valueAsNumber)} | ||||||
| /> | ||||||
| {field.state.meta.errors.map((error) => ( | ||||||
| <p key={error as string}>{error}</p> | ||||||
| ))} | ||||||
| </div> | ||||||
| ) | ||||||
| }} | ||||||
| </form.Field> | ||||||
| <form.Subscribe | ||||||
| selector={(formState) => [formState.canSubmit, formState.isSubmitting]} | ||||||
| > | ||||||
| {([canSubmit, isSubmitting]) => ( | ||||||
| <button type="submit" disabled={!canSubmit}> | ||||||
| {isSubmitting ? '...' : 'Submit'} | ||||||
| </button> | ||||||
| )} | ||||||
| </form.Subscribe> | ||||||
| </form> | ||||||
| ) | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ### useTransform | ||||||
|
|
||||||
| 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. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This change needs to be applied to the other SSR docs. |
||||||
|
|
||||||
| ## debugging | ||||||
|
|
||||||
| > If you get the following error in your Next.js application: | ||||||
| > | ||||||
| > ```typescript | ||||||
| > 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. | ||||||
| > ``` | ||||||
| > | ||||||
| > 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. | ||||||
| > | ||||||
| > [This is a limitation of Next.js](https://github.com/phryneas/rehackt). Other meta-frameworks will likely not have this same problem. | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| --- | ||
| id: ssr | ||
| title: TanStack Form - Remix | ||
| --- | ||
|
|
||
| ## Using TanStack Form in Remix | ||
|
|
||
| > 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) | ||
|
|
||
| ### Remix Prerequisites | ||
|
|
||
| - Start a new `Remix` project, following the steps in the [Remix Documentation](https://remix.run/docs/en/main/start/quickstart). | ||
| - Install `@tanstack/react-form` | ||
| - Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] | ||
|
|
||
| ## Remix integration | ||
|
|
||
| Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. | ||
|
|
||
| ```tsx routes/_index/route.tsx | ||
| import { formOptions } from '@tanstack/react-form-remix' | ||
|
|
||
| // You can pass other form options here | ||
| export const formOpts = formOptions({ | ||
| defaultValues: { | ||
| firstName: '', | ||
| age: 0, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| ```tsx routes/_index/route.tsx | ||
| import { | ||
| ServerValidateError, | ||
| createServerValidate, | ||
| formOptions, | ||
| } from '@tanstack/react-form-remix' | ||
|
|
||
| import type { ActionFunctionArgs } from '@remix-run/node' | ||
|
|
||
| // Create the server action that will infer the types of the form from `formOpts` | ||
| const serverValidate = createServerValidate({ | ||
| ...formOpts, | ||
| onServerValidate: ({ value }) => { | ||
| if (value.age < 12) { | ||
| return 'Server validation: You must be at least 12 to sign up' | ||
| } | ||
| }, | ||
| }) | ||
|
|
||
| export async function action({ request }: ActionFunctionArgs) { | ||
| const formData = await request.formData() | ||
| try { | ||
| const validatedData = await serverValidate(formData) | ||
| console.log('validatedData', validatedData) | ||
| // Persist the form data to the database | ||
| // await sql` | ||
| // INSERT INTO users (name, email, password) | ||
| // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) | ||
| // ` | ||
| } catch (e) { | ||
| if (e instanceof ServerValidateError) { | ||
| return e.formState | ||
| } | ||
|
|
||
| // Some other error occurred while validating your form | ||
| throw e | ||
| } | ||
|
|
||
| // Your form has successfully validated! | ||
| } | ||
| ``` | ||
|
|
||
| Finally, the `action` will be called when the form submits. | ||
|
|
||
| ```tsx | ||
| // routes/_index/route.tsx | ||
| import { Form, useActionData } from '@remix-run/react' | ||
| import { mergeForm, useForm, useStore } from '@tanstack/react-form' | ||
| import { | ||
| ServerValidateError, | ||
| createServerValidate, | ||
| formOptions, | ||
| initialFormState, | ||
| useTransform, | ||
| } from '@tanstack/react-form-remix' | ||
|
|
||
| export default function Index() { | ||
| const actionData = useActionData<typeof action>() | ||
|
|
||
| const form = useForm({ | ||
| ...formOpts, | ||
| transform: useTransform( | ||
| (baseForm) => mergeForm(baseForm, actionData ?? initialFormState), | ||
| [actionData], | ||
| ), | ||
| }) | ||
|
|
||
| const formErrors = useStore(form.store, (formState) => formState.errors) | ||
|
|
||
| return ( | ||
| <form method="post" onSubmit={() => form.handleSubmit()}> | ||
| {formErrors.map((error) => ( | ||
| <p key={error as string}>{error}</p> | ||
| ))} | ||
|
|
||
| <form.Field | ||
| name="age" | ||
| validators={{ | ||
| onChange: ({ value }) => | ||
| value < 8 ? 'Client validation: You must be at least 8' : undefined, | ||
| }} | ||
| > | ||
| {(field) => { | ||
| return ( | ||
| <div> | ||
| <input | ||
| name="age" | ||
| type="number" | ||
| value={field.state.value} | ||
| onChange={(e) => field.handleChange(e.target.valueAsNumber)} | ||
| /> | ||
| {field.state.meta.errors.map((error) => ( | ||
| <p key={error as string}>{error}</p> | ||
| ))} | ||
| </div> | ||
| ) | ||
| }} | ||
| </form.Field> | ||
| <form.Subscribe | ||
| selector={(formState) => [formState.canSubmit, formState.isSubmitting]} | ||
| > | ||
| {([canSubmit, isSubmitting]) => ( | ||
| <button type="submit" disabled={!canSubmit}> | ||
| {isSubmitting ? '...' : 'Submit'} | ||
| </button> | ||
| )} | ||
| </form.Subscribe> | ||
| </form> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| ### useTransform | ||
|
|
||
| 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. | ||
|
|
||
| ```tsx | ||
| const form = useForm({ | ||
| ...formOpts, | ||
| transform: useTransform( | ||
| (baseForm) => mergeForm(baseForm, actionData ?? initialFormState), | ||
| [actionData], | ||
| ), | ||
| }) | ||
| ``` |
Uh oh!
There was an error while loading. Please reload this page.