Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion docs/framework/react/guides/form-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ function App() {
}
```

This not only allows you to reuse the UI of your shared component, but retains the type-safety you'd expect from TanStack Form: Mistyping `name` will result in a TypeScript error.
This not only allows you to reuse the UI of your shared component, but retains the type-safety you'd expect from TanStack Form: Mistyping `firstName` will result in a TypeScript error.

#### A note on performance

Expand Down Expand Up @@ -160,6 +160,68 @@ function App() {
}
```

### Extending custom appForm

It is quite common for platform teams to ship pre built appForms. It can be exported from a library in a monorepo or as a standalone package on npm.

```tsx weyland-yutan-corp/forms.tsx
import { createFormHook, createFormHookContexts } from '@tanstack/react-form'

// fields
import { UserIdField } from './FieldComponents/UserIdField'

// components
import { SubmitButton } from './FormComponents/SubmitButton'

export const { fieldContext, formContext, useFieldContext, useFormContext } =
createFormHookContexts()

const ProfileForm = createFormHook({
fieldContext,
formContext,
fieldComponents: { UserIdField },
formComponents: { SubmitButton },
})

export default ProfileForm
```

There is a situation that you might have a field exclusive to a downstream dev team, in such a case you can extend the AppForm like so.

1 - Create new AppForm fields

```tsx AppForm.tsx
// imported from the same AppForm you want to extend
import { useFieldContext } from 'weyland-yutan-corp/forms'

export function CustomTextField({ label }: { label: string }) {
const field = useFieldContext<string>()
return (
<div>
<label>{/* rest of component */}</label>
</div>
)
}
```

2 - Extend the AppForm

```tsx AppForm.tsx
// notice the same import as above
import ProfileForm from 'weyland-yutan-corp/forms'

import { CustomTextField } from './FieldComponents/CustomTextField'
import { SubmitButton } from './FormComponents/SubmitButton'

export const { useAppForm } = ProfileForm.extendForm({
fieldComponents: { CustomTextField },
// Ts will error since the parent appForm already has a component called SubmitButton
formComponents: { SubmitButton },
})
```

This way you can add extra fields that are unique to your team without bloating the upstream AppForm.
Comment on lines +216 to +223
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Primary extension example currently demonstrates a failing snippet.

At Line 219 the sample intentionally triggers a TS collision, but this appears in the main “how to extend” flow and is followed by success-oriented text. Please make the primary snippet valid (unique component name), and move the collision case to a separate “gotcha” block.

Suggested patch
 import ProfileForm from 'weyland-yutan-corp/forms'
 
 import { CustomTextField } from './FieldComponents/CustomTextField'
-import { SubmitButton } from './FormComponents/SubmitButton'
+import { CustomSubmitButton } from './FormComponents/CustomSubmitButton'
 
 export const { useAppForm } = ProfileForm.extendForm({
   fieldComponents: { CustomTextField },
-  // Ts will error since the parent appForm already has a component called SubmitButton
-  formComponents: { SubmitButton },
+  formComponents: { CustomSubmitButton },
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/framework/react/guides/form-composition.md` around lines 216 - 223, The
primary example shows a TypeScript collision by reusing SubmitButton; update the
main snippet so ProfileForm.extendForm uses a uniquely named component (e.g.,
formComponents: { TeamSubmitButton } or another distinct identifier) and expose
useAppForm from that result; then move the collision example (reusing
SubmitButton) into a separate "Gotcha" or "Type collision" block with an
explanatory note. Target the ProfileForm.extendForm call, the exported
useAppForm binding, and the component names CustomTextField and SubmitButton
when making the edits.


## Breaking big forms into smaller pieces

Sometimes forms get very large; it's just how it goes sometimes. While TanStack Form supports large forms well, it's never fun to work with hundreds or thousands of lines of code in single files.
Expand Down
11 changes: 11 additions & 0 deletions examples/react/composition/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-check

/** @type {import('eslint').Linter.Config} */
const config = {
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
rules: {
'react/no-children-prop': 'off',
},
}

module.exports = config
27 changes: 27 additions & 0 deletions examples/react/composition/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

pnpm-lock.yaml
yarn.lock
package-lock.json
Comment on lines +14 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Lockfiles should be committed for reproducible builds.

Ignoring all package manager lockfiles (pnpm-lock.yaml, yarn.lock, package-lock.json) violates modern JavaScript best practices. Lockfiles ensure consistent dependency versions across development environments and deployments. Since this is an example project, it should demonstrate the recommended practice of committing the lockfile for whichever package manager is in use.

📦 Recommended fix

Keep only the lockfiles you don't use:

-pnpm-lock.yaml
-yarn.lock
-package-lock.json
+# Commit the lockfile for your package manager
+# If using pnpm, ignore these:
+yarn.lock
+package-lock.json
+
+# If using yarn, ignore these:
+# pnpm-lock.yaml
+# package-lock.json
+
+# If using npm, ignore these:
+# pnpm-lock.yaml
+# yarn.lock
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pnpm-lock.yaml
yarn.lock
package-lock.json
# Commit the lockfile for your package manager
# If using pnpm, ignore these:
yarn.lock
package-lock.json
# If using yarn, ignore these:
# pnpm-lock.yaml
# package-lock.json
# If using npm, ignore these:
# pnpm-lock.yaml
# yarn.lock
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/composition/.gitignore` around lines 14 - 16, The .gitignore
currently excludes pnpm-lock.yaml, yarn.lock, and package-lock.json which
prevents committing lockfiles; update the .gitignore in
examples/react/composition to stop ignoring the lockfile your example uses
(e.g., remove pnpm-lock.yaml if the project uses pnpm, or remove
yarn.lock/package-lock.json for their respective managers) so the appropriate
lockfile is committed for reproducible installs; ensure only irrelevant
lockfiles remain listed (or none) so the correct lockfile (pnpm-lock.yaml or
yarn.lock or package-lock.json) is tracked in Git.


# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
6 changes: 6 additions & 0 deletions examples/react/composition/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install`
- `npm run dev`
Comment on lines +3 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Specify the working directory before run commands.

The commands are ambiguous as written. Add an explicit cd examples/react/composition step to avoid running them from repo root.

Suggested patch
 To run this example:
 
+- `cd examples/react/composition`
 - `npm install`
 - `npm run dev`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
To run this example:
- `npm install`
- `npm run dev`
To run this example:
- `cd examples/react/composition`
- `npm install`
- `npm run dev`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/composition/README.md` around lines 3 - 6, Update the "To run
this example:" instructions to explicitly set the working directory before
running npm commands: add a step like "cd examples/react/composition" (or
similar wording) before the existing `npm install` and `npm run dev` lines so
users know to change into the example directory; ensure the "To run this
example:" section in README.md includes that cd step and that the subsequent
commands remain in the same ordered list.

16 changes: 16 additions & 0 deletions examples/react/composition/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

<title>TanStack Form React Composition Example App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions examples/react/composition/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@tanstack/form-example-react-composition",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"preview": "vite preview",
"test:types": "tsc"
},
"dependencies": {
"@tanstack/react-form": "^1.28.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tanstack/react-devtools": "^0.9.7",
"@tanstack/react-form-devtools": "^0.2.20",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^5.1.1",
"vite": "^7.2.2"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
13 changes: 13 additions & 0 deletions examples/react/composition/public/emblem-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions examples/react/composition/src/AppForm/AppForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createFormHook, createFormHookContexts } from '@tanstack/react-form'

//
// fields
//
import { TextField } from './FieldComponents/TextField'

//
// components
//
import { SubmitButton } from './FormComponents/SubmitButton'

export const { fieldContext, formContext, useFieldContext, useFormContext } =
createFormHookContexts()

const { useAppForm, extendForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TextField },
formComponents: { SubmitButton },
})

export { extendForm }
export default useAppForm
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useFieldContext } from '../AppForm'

export function TextField({ label }: { label: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<label>
<span>{label}</span>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>

<>
{field.state.meta.isTouched && !field.state.meta.isValid ? (
<em>{field.state.meta.errors.join(',')}</em>
) : null}
{field.state.meta.isValidating ? 'Validating...' : null}
</>
</label>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useFormContext } from '../AppForm'

export function SubmitButton({ label }: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{label}
</button>
)}
</form.Subscribe>
)
}
77 changes: 77 additions & 0 deletions examples/react/composition/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react'
import { createRoot } from 'react-dom/client'

import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'

import useAppForm from './AppForm/AppForm'

export default function App() {
const form = useAppForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})

return (
<div>
<h1>Simple Form Example</h1>

<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
{/* A type-safe field component*/}
<form.AppField
name="firstName"
validators={{
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error')
? 'No "error" allowed in first name'
: undefined
},
}}
>
{(f) => <f.TextField label="First Name" />}
</form.AppField>

<form.AppField name="lastName">
{(f) => <f.TextField label="Last Name" />}
</form.AppField>

<form.AppForm>
<form.SubmitButton label="save" />
</form.AppForm>
</form>
</div>
)
}

const rootElement = document.getElementById('root')!

createRoot(rootElement).render(
<React.StrictMode>
<App />

<TanStackDevtools
config={{ hideUntilHover: true }}
plugins={[formDevtoolsPlugin()]}
/>
</React.StrictMode>,
)
23 changes: 23 additions & 0 deletions examples/react/composition/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Loading