-
-
Notifications
You must be signed in to change notification settings - Fork 621
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
Open
harry-whorlow
wants to merge
6
commits into
main
Choose a base branch
from
ssr-docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f9616f9
docs(react-remix-form): fix useTransform import
harry-whorlow cdf783c
docs(arrays): add array key docs
harry-whorlow 0842cff
chore: update ssr
harry-whorlow 707b5ed
docs: add canSubmit invalid docs
harry-whorlow aff13ab
chore: pr comments
harry-whorlow efa1fe7
chore: pr comments
harry-whorlow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,162 @@ | ||||||
| --- | ||||||
| id: nextjs | ||||||
| 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. | ||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| --- | ||
| id: remix | ||
| 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-start` | ||
| - 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, its 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], | ||
| ), | ||
| }) | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah when was that changed?