Skip to content

Commit d6ab662

Browse files
feat(react-from): extend appform (#2106)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4276176 commit d6ab662

17 files changed

Lines changed: 782 additions & 3 deletions

File tree

.changeset/neat-jars-rescue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/react-form': minor
3+
---
4+
5+
Adds extension method to AppForm allowing for teams to extend upstream AppForms

docs/framework/react/guides/form-composition.md

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ At it's most basic, `createFormHook` is a function that takes a `fieldContext` a
1515

1616
> This un-customized `useAppForm` hook is identical to `useForm`, but that will quickly change as we add more options to `createFormHook`.
1717
18-
```tsx
19-
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
18+
```tsx AppFormContext.tsx
19+
import { createFormHookContexts } from '@tanstack/react-form'
2020

2121
// export useFieldContext for use in your custom components
2222
export const { fieldContext, formContext, useFieldContext } =
2323
createFormHookContexts()
24+
```
25+
26+
```tsx AppForm.tsx
27+
import { createFormHook } from '@tanstack/react-form'
28+
import { createFormHookContexts } from './AppFormContext'
2429

2530
const { useAppForm } = createFormHook({
2631
fieldContext,
@@ -103,7 +108,7 @@ function App() {
103108
}
104109
```
105110

106-
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.
111+
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.
107112

108113
#### A note on performance
109114

@@ -160,6 +165,71 @@ function App() {
160165
}
161166
```
162167

168+
### Extending custom appForm
169+
170+
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.
171+
172+
```tsx weyland-yutan-corp/forms-context.tsx
173+
export const { fieldContext, formContext, useFieldContext, useFormContext } =
174+
createFormHookContexts()
175+
```
176+
177+
```tsx weyland-yutan-corp/forms.tsx
178+
import { createFormHook } from '@tanstack/react-form'
179+
import { fieldContext, formContext } from 'weyland-yutan-corp/forms-context'
180+
181+
// fields
182+
import { UserIdField } from './FieldComponents/UserIdField'
183+
184+
// components
185+
import { SubmitButton } from './FormComponents/SubmitButton'
186+
187+
const ProfileForm = createFormHook({
188+
fieldContext,
189+
formContext,
190+
fieldComponents: { UserIdField },
191+
formComponents: { SubmitButton },
192+
})
193+
194+
export default ProfileForm
195+
```
196+
197+
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.
198+
199+
1 - Create new AppForm fields
200+
201+
```tsx AppForm.tsx
202+
// imported from the same AppForm you want to extend
203+
import { useFieldContext } from 'weyland-yutan-corp/forms-context'
204+
205+
export function CustomTextField({ label }: { label: string }) {
206+
const field = useFieldContext<string>()
207+
return (
208+
<div>
209+
<label>{/* rest of component */}</label>
210+
</div>
211+
)
212+
}
213+
```
214+
215+
2 - Extend the AppForm
216+
217+
```tsx AppForm.tsx
218+
// notice the same import as above
219+
import ProfileForm from 'weyland-yutan-corp/forms'
220+
221+
import { CustomTextField } from './FieldComponents/CustomTextField'
222+
import { SubmitButton } from './FormComponents/SubmitButton'
223+
224+
export const { useAppForm } = ProfileForm.extendForm({
225+
fieldComponents: { CustomTextField },
226+
// Ts will error since the parent appForm already has a component called CustomSubmit
227+
formComponents: { SubmitButton },
228+
})
229+
```
230+
231+
This way you can add extra fields that are unique to your team without bloating the upstream AppForm.
232+
163233
## Breaking big forms into smaller pieces
164234

165235
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.
@@ -218,6 +288,9 @@ function App() {
218288
}
219289
```
220290

291+
> Something worth mentioning, is that while multiple chaining of `AppForm` extensions is possible it is can lead to decreases in TypeScript performance.
292+
> For most users that may only extend an appForm once this isn't a problem, however we recommend limiting it to 3-5 extensions.
293+
221294
### `withForm` FAQ
222295

223296
> Why a higher-order component instead of a hook?
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @ts-check
2+
3+
/** @type {import('eslint').Linter.Config} */
4+
const config = {
5+
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
6+
rules: {
7+
'react/no-children-prop': 'off',
8+
},
9+
}
10+
11+
module.exports = config
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
pnpm-lock.yaml
15+
yarn.lock
16+
package-lock.json
17+
18+
# misc
19+
.DS_Store
20+
.env.local
21+
.env.development.local
22+
.env.test.local
23+
.env.production.local
24+
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
9+
<title>TanStack Form React Simple Example App</title>
10+
</head>
11+
<body>
12+
<noscript>You need to enable JavaScript to run this app.</noscript>
13+
<div id="root"></div>
14+
<script type="module" src="/src/index.tsx"></script>
15+
</body>
16+
</html>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@tanstack/form-example-react-composition",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port=3001",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "tsc"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-form": "^1.28.6",
13+
"react": "^19.0.0",
14+
"react-dom": "^19.0.0"
15+
},
16+
"devDependencies": {
17+
"@tanstack/react-devtools": "^0.9.7",
18+
"@tanstack/react-form-devtools": "^0.2.20",
19+
"@types/react": "^19.0.7",
20+
"@types/react-dom": "^19.0.3",
21+
"@vitejs/plugin-react": "^5.1.1",
22+
"vite": "^7.2.2"
23+
},
24+
"browserslist": {
25+
"production": [
26+
">0.2%",
27+
"not dead",
28+
"not op_mini all"
29+
],
30+
"development": [
31+
"last 1 chrome version",
32+
"last 1 firefox version",
33+
"last 1 safari version"
34+
]
35+
}
36+
}
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createFormHook, createFormHookContexts } from '@tanstack/react-form'
2+
3+
//
4+
// fields
5+
//
6+
import { TextField } from './FieldComponents/TextField'
7+
8+
//
9+
// components
10+
//
11+
import { SubmitButton } from './FormComponents/SubmitButton'
12+
13+
export const { fieldContext, formContext, useFieldContext, useFormContext } =
14+
createFormHookContexts()
15+
16+
const { useAppForm } = createFormHook({
17+
fieldContext,
18+
formContext,
19+
fieldComponents: { TextField: TextField },
20+
formComponents: { SubmitButton },
21+
})
22+
23+
export default useAppForm
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useFieldContext } from '../AppForm'
2+
3+
export function TextField({ label }: { label: string }) {
4+
// The `Field` infers that it should have a `value` type of `string`
5+
const field = useFieldContext<string>()
6+
return (
7+
<label>
8+
<span>{label}</span>
9+
<input
10+
value={field.state.value}
11+
onChange={(e) => field.handleChange(e.target.value)}
12+
onBlur={() => field.handleBlur()}
13+
/>
14+
15+
<>
16+
{field.state.meta.isTouched && !field.state.meta.isValid ? (
17+
<em>{field.state.meta.errors.join(',')}</em>
18+
) : null}
19+
{field.state.meta.isValidating ? 'Validating...' : null}
20+
</>
21+
</label>
22+
)
23+
}

0 commit comments

Comments
 (0)