-
-
Notifications
You must be signed in to change notification settings - Fork 610
fix(react-form): address review issues from #2106 (extendForm / composition example) #2115
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 all commits
7519997
e79b4f2
4a49d2e
94d3397
6323166
23d9e03
6263287
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 |
|---|---|---|
| @@ -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 |
| 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
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. 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 fixKeep 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # misc | ||||||||||||||||||||||||||||||||
| .DS_Store | ||||||||||||||||||||||||||||||||
| .env.local | ||||||||||||||||||||||||||||||||
| .env.development.local | ||||||||||||||||||||||||||||||||
| .env.test.local | ||||||||||||||||||||||||||||||||
| .env.production.local | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| npm-debug.log* | ||||||||||||||||||||||||||||||||
| yarn-debug.log* | ||||||||||||||||||||||||||||||||
| yarn-error.log* | ||||||||||||||||||||||||||||||||
| 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
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. Specify the working directory before run commands. The commands are ambiguous as written. Add an explicit Suggested patch To run this example:
+- `cd examples/react/composition`
- `npm install`
- `npm run dev`📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| 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> |
| 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" | ||
| ] | ||
| } | ||
| } |
| 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> | ||
| ) | ||
| } |
| 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>, | ||
| ) |
| 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"] | ||
| } |
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.
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