Conversation
📝 WalkthroughWalkthroughThis pull request introduces form composition capabilities to TanStack React Form. It adds an Changes
Sequence Diagram(s)sequenceDiagram
participant Platform as Platform Team
participant Hook as createFormHook()
participant Extend as extendForm()
participant Downstream as Downstream Team
participant NewHook as Extended Hook
Platform->>Hook: createFormHook({<br/>fieldComponents: {TextField},<br/>formComponents: {SubmitButton}<br/>})
Hook-->>Platform: baseHook with base components
Downstream->>Extend: baseHook.extendForm({<br/>fieldComponents: {CustomField},<br/>formComponents: {CustomForm}<br/>})
Note over Extend: Merge base + extended<br/>components<br/>Check key uniqueness
Extend-->>Downstream: New Hook instance
Downstream->>NewHook: useNewHook()
Note over NewHook: Shared contexts<br/>Merged component maps<br/>(TextField, CustomField,<br/>SubmitButton, CustomForm)
NewHook-->>Downstream: Form with all components
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ❌ 3❌ Failed checks (3 warnings)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit 23d9e03
☁️ Nx Cloud last updated this comment at |
🚀 Changeset Version Preview1 package(s) bumped directly, 12 bumped as dependents. 🟩 Patch bumps
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2106 +/- ##
==========================================
- Coverage 90.35% 90.25% -0.10%
==========================================
Files 38 49 +11
Lines 1752 2043 +291
Branches 444 532 +88
==========================================
+ Hits 1583 1844 +261
- Misses 149 179 +30
Partials 20 20 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
packages/react-form/tests/createFormHook.test-d.tsx (1)
993-1021: The chaining type test never provesThirdFieldsurvived the second extension.This case creates
ThirdField, but the assertions still only coverTestandExtendedField, then stop atexpectTypeOf(doublyExtendedForm.AppField).toBeFunction(). A regression where the secondextendFormdrops the new key would still pass.Suggested fix
- const { useAppForm: useDoublyExtended } = baseHook + const { + useAppForm: useDoublyExtended, + withForm: withDoublyExtendedForm, + } = baseHook .extendForm({ fieldComponents: { ExtendedField } }) .extendForm({ fieldComponents: { ThirdField } }) - withExtendedForm({ + withDoublyExtendedForm({ defaultValues: { name: '' }, render: ({ form }) => { return ( <form.AppField name="name"> {(field) => { expectTypeOf(field.Test).toBeFunction() expectTypeOf(field.ExtendedField).toBeFunction() + expectTypeOf(field.ThirdField).toBeFunction() return null }} </form.AppField> ) }, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-form/tests/createFormHook.test-d.tsx` around lines 993 - 1021, The test never asserts that ThirdField survived the second extendForm call; update the test around useDoublyExtended/withExtendedForm to assert ThirdField is present — add expectTypeOf(field.ThirdField).toBeFunction() inside the render callback (alongside the existing Test and ExtendedField checks) and also assert the doublyExtendedForm exposes the extended field (e.g., an assertion that the useDoublyExtended result exposes ThirdField on AppField or the equivalent exported shape) so the chaining regression would fail if the second extension drops the key.examples/react/composition/index.html (1)
9-9: Title doesn't match the example name.The title says "Simple Example App" but this is the composition example. Consider updating to reflect the actual example purpose.
- <title>TanStack Form React Simple Example App</title> + <title>TanStack Form React Composition Example App</title>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/composition/index.html` at line 9, Update the HTML <title> element to reflect this is the "Composition" example rather than "Simple Example App": change the title text content inside the <title> tag (currently "TanStack Form React Simple Example App") to something like "TanStack Form React Composition Example" so it accurately describes this example page.examples/react/composition/src/index.tsx (2)
43-48: Async validator returnsfalseinstead ofundefinedwhen valid.When
value.includes('error')is false, the&&short-circuit returnsfalse. While TanStack Form may handle this, explicitly returningundefinedfor valid state is clearer and more conventional.onChangeAsync: async ({ value }) => { await new Promise((resolve) => setTimeout(resolve, 1000)) - return ( - value.includes('error') && 'No "error" allowed in first name' - ) + return value.includes('error') + ? 'No "error" allowed in first name' + : undefined },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/composition/src/index.tsx` around lines 43 - 48, The async validator onChangeAsync currently returns the string error when value.includes('error') is true but returns false when valid due to the && expression; update the onChangeAsync implementation (the async validator function) to explicitly return undefined for valid input instead of false—e.g., replace the short-circuit expression with a conditional (ternary or if) that returns 'No "error" allowed in first name' when value.includes('error') is true and undefined otherwise.
54-56: Consider using a distinct label for the lastName field.Both fields currently display "last name". The second one is correct, but you may want to capitalize it consistently (e.g., "Last Name").
- {(f) => <f.TextField label="last name" />} + {(f) => <f.TextField label="Last Name" />}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/composition/src/index.tsx` around lines 54 - 56, The lastName field's visible label is not distinct/capitalized; update the form.AppField for name="lastName" to use a proper, distinct label (e.g., change the f.TextField label prop to "Last Name") and verify the other field (form.AppField name="firstName") uses "First Name" so both fields are correctly labeled and consistently capitalized; look for usages of form.AppField and the f.TextField label prop to make this change.examples/react/composition/src/AppForm/AppForm.tsx (1)
16-21: Consider exportingextendFormfor downstream composition.Given this PR introduces form composition capabilities via
extendForm, you might want to export it alongsideuseAppFormso downstream consumers can extend this form with additional components.-const { useAppForm } = createFormHook({ +const { useAppForm, extendForm } = createFormHook({ fieldContext, formContext, fieldComponents: { TextField: TextField }, formComponents: { SubmitButton }, }) + +export { extendForm }Also, minor:
TextField: TextFieldcan use object shorthand:TextField.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/composition/src/AppForm/AppForm.tsx` around lines 16 - 21, The export currently only exposes useAppForm from createFormHook; update the destructuring returned by createFormHook to also export extendForm so downstream modules can compose/extend the form (referencing createFormHook, useAppForm, extendForm), and simplify the fieldComponents object by using shorthand for TextField (replace TextField: TextField with TextField) while keeping SubmitButton in formComponents.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/framework/react/guides/form-composition.md`:
- Around line 209-220: The example is incorrect: the package import casing was
changed and the example claims a name collision with CustomSubmit even though
ProfileForm defines SubmitButton; revert the import to the original casing
('weyland-yutan-corp/forms') and demonstrate a real collision by using the same
symbol name that ProfileForm actually exposes (e.g., use SubmitButton instead of
CustomSubmit) so TypeScript will error; update the export line that calls
ProfileForm.extendForm (reference: ProfileForm, useAppForm, CustomTextField,
CustomSubmit, SubmitButton) to import from the correctly-cased package and to
pass a formComponents object that actually collides with the parent
(SubmitButton) rather than CustomSubmit.
In `@examples/react/composition/src/AppForm/FieldComponents/TextField.tsx`:
- Around line 9-12: The TextField input is missing an onBlur handler so
field.state.meta.isTouched and onBlur validators won't update; update the input
rendered in the TextField component to add an onBlur that calls the form field's
blur handler (e.g., invoke field.handleBlur or the appropriate blur method on
the field object) so the field's touched state and onBlur validation run
correctly.
In `@examples/react/composition/src/index.tsx`:
- Around line 50-52: The firstName field in the form is using the wrong label;
update the AppField rendering for the firstName field (the instance that calls
form.AppField and renders {(f) => <f.TextField label="last name" />} ) to use
the correct label "first name" (i.e., change the label prop on f.TextField to
"first name" for the firstName AppField).
In `@packages/react-form/src/createFormHook.tsx`:
- Around line 599-617: extendForm currently only prevents extending keys that
exist in the parent maps but doesn't block names that collide with core runtime
APIs (e.g., AppForm, AppField, Field, Subscribe, handleChange) and thus can
silently override core behavior; modify extendForm to declare a reservedNames
set (include core exported API and runtime member names) and validate
extension.fieldComponents and extension.formComponents against that set,
rejecting/throwing with a clear error if any key from TNewField or TNewForm
appears in reservedNames; reference the extendForm function and the
fieldComponents/formComponents merge locations so the checks run before calling
createFormHook and before the cast to TComponents & TNewField / TFormComponents
& TNewForm.
---
Nitpick comments:
In `@examples/react/composition/index.html`:
- Line 9: Update the HTML <title> element to reflect this is the "Composition"
example rather than "Simple Example App": change the title text content inside
the <title> tag (currently "TanStack Form React Simple Example App") to
something like "TanStack Form React Composition Example" so it accurately
describes this example page.
In `@examples/react/composition/src/AppForm/AppForm.tsx`:
- Around line 16-21: The export currently only exposes useAppForm from
createFormHook; update the destructuring returned by createFormHook to also
export extendForm so downstream modules can compose/extend the form (referencing
createFormHook, useAppForm, extendForm), and simplify the fieldComponents object
by using shorthand for TextField (replace TextField: TextField with TextField)
while keeping SubmitButton in formComponents.
In `@examples/react/composition/src/index.tsx`:
- Around line 43-48: The async validator onChangeAsync currently returns the
string error when value.includes('error') is true but returns false when valid
due to the && expression; update the onChangeAsync implementation (the async
validator function) to explicitly return undefined for valid input instead of
false—e.g., replace the short-circuit expression with a conditional (ternary or
if) that returns 'No "error" allowed in first name' when value.includes('error')
is true and undefined otherwise.
- Around line 54-56: The lastName field's visible label is not
distinct/capitalized; update the form.AppField for name="lastName" to use a
proper, distinct label (e.g., change the f.TextField label prop to "Last Name")
and verify the other field (form.AppField name="firstName") uses "First Name" so
both fields are correctly labeled and consistently capitalized; look for usages
of form.AppField and the f.TextField label prop to make this change.
In `@packages/react-form/tests/createFormHook.test-d.tsx`:
- Around line 993-1021: The test never asserts that ThirdField survived the
second extendForm call; update the test around
useDoublyExtended/withExtendedForm to assert ThirdField is present — add
expectTypeOf(field.ThirdField).toBeFunction() inside the render callback
(alongside the existing Test and ExtendedField checks) and also assert the
doublyExtendedForm exposes the extended field (e.g., an assertion that the
useDoublyExtended result exposes ThirdField on AppField or the equivalent
exported shape) so the chaining regression would fail if the second extension
drops the key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 89488856-d987-469f-8a56-da5c672e2719
⛔ Files ignored due to path filters (2)
examples/react/composition/public/emblem-light.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
docs/framework/react/guides/form-composition.mdexamples/react/composition/.eslintrc.cjsexamples/react/composition/.gitignoreexamples/react/composition/README.mdexamples/react/composition/index.htmlexamples/react/composition/package.jsonexamples/react/composition/src/AppForm/AppForm.tsxexamples/react/composition/src/AppForm/FieldComponents/TextField.tsxexamples/react/composition/src/AppForm/FormComponents/SubmitButton.tsxexamples/react/composition/src/index.tsxexamples/react/composition/tsconfig.jsonpackages/react-form/src/createFormHook.tsxpackages/react-form/tests/createFormHook.test-d.tsxpackages/react-form/tests/createFormHook.test.tsx
| ```tsx AppForm.tsx | ||
| // notice the same import as above | ||
| import ProfileForm from 'Weyland-Yutan-corp/forms' | ||
|
|
||
| import { CustomTextField } from './FieldComponents/CustomTextField' | ||
| import { CustomSubmit } from './FormComponents/CustomSubmit' | ||
|
|
||
| export const { useAppForm } = ProfileForm.extendForm({ | ||
| fieldComponents: { CustomTextField }, | ||
| // Ts will error since the parent appForm already has a component called CustomSubmit | ||
| formComponents: { CustomSubmit }, | ||
| }) |
There was a problem hiding this comment.
The duplicate-name example is wrong, and the import casing changes.
Line 218 says TypeScript should reject CustomSubmit, but ProfileForm only defines SubmitButton in the earlier example, so there is no actual collision here. Line 211 also changes the package name casing from weyland-yutan-corp/forms, which will not resolve on case-sensitive installs.
Suggested fix
-// notice the same import as above
-import ProfileForm from 'Weyland-Yutan-corp/forms'
+// notice the same import as above
+import ProfileForm from 'weyland-yutan-corp/forms'
import { CustomTextField } from './FieldComponents/CustomTextField'
-import { CustomSubmit } from './FormComponents/CustomSubmit'
export const { useAppForm } = ProfileForm.extendForm({
fieldComponents: { CustomTextField },
- // Ts will error since the parent appForm already has a component called CustomSubmit
- formComponents: { CustomSubmit },
+ // TS will error if you reuse an existing key from the base form, e.g.:
+ // formComponents: { SubmitButton },
})🤖 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 209 - 220, The
example is incorrect: the package import casing was changed and the example
claims a name collision with CustomSubmit even though ProfileForm defines
SubmitButton; revert the import to the original casing
('weyland-yutan-corp/forms') and demonstrate a real collision by using the same
symbol name that ProfileForm actually exposes (e.g., use SubmitButton instead of
CustomSubmit) so TypeScript will error; update the export line that calls
ProfileForm.extendForm (reference: ProfileForm, useAppForm, CustomTextField,
CustomSubmit, SubmitButton) to import from the correctly-cased package and to
pass a formComponents object that actually collides with the parent
(SubmitButton) rather than CustomSubmit.
| <input | ||
| value={field.state.value} | ||
| onChange={(e) => field.handleChange(e.target.value)} | ||
| /> |
There was a problem hiding this comment.
Missing onBlur handler may prevent validation state from updating correctly.
The input lacks an onBlur handler. Without it, field.state.meta.isTouched may not be set as expected, and onBlur validators won't trigger.
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
+ onBlur={() => field.handleBlur()}
/>📝 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.
| <input | |
| value={field.state.value} | |
| onChange={(e) => field.handleChange(e.target.value)} | |
| /> | |
| <input | |
| value={field.state.value} | |
| onChange={(e) => field.handleChange(e.target.value)} | |
| onBlur={() => field.handleBlur()} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/react/composition/src/AppForm/FieldComponents/TextField.tsx` around
lines 9 - 12, The TextField input is missing an onBlur handler so
field.state.meta.isTouched and onBlur validators won't update; update the input
rendered in the TextField component to add an onBlur that calls the form field's
blur handler (e.g., invoke field.handleBlur or the appropriate blur method on
the field object) so the field's touched state and onBlur validation run
correctly.
| > | ||
| {(f) => <f.TextField label="last name" />} | ||
| </form.AppField> |
There was a problem hiding this comment.
Incorrect label: "last name" is used for the firstName field.
This appears to be a copy-paste error. The firstName field displays "last name" as its label.
- {(f) => <f.TextField label="last name" />}
+ {(f) => <f.TextField label="First Name" />}📝 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.
| > | |
| {(f) => <f.TextField label="last name" />} | |
| </form.AppField> | |
| > | |
| {(f) => <f.TextField label="First Name" />} | |
| </form.AppField> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/react/composition/src/index.tsx` around lines 50 - 52, The firstName
field in the form is using the wrong label; update the AppField rendering for
the firstName field (the instance that calls form.AppField and renders {(f) =>
<f.TextField label="last name" />} ) to use the correct label "first name"
(i.e., change the label prop on f.TextField to "first name" for the firstName
AppField).
| function extendForm< | ||
| const TNewField extends Record<string, ComponentType<any>> & { | ||
| [K in keyof TComponents]?: 'Error: field component names must be unique — this key already exists in the base form' | ||
| }, | ||
| const TNewForm extends Record<string, ComponentType<any>> & { | ||
| [K in keyof TFormComponents]?: 'Error: form component names must be unique — this key already exists in the base form' | ||
| }, | ||
| >(extension: { fieldComponents?: TNewField; formComponents?: TNewForm }) { | ||
| return createFormHook({ | ||
| fieldContext, | ||
| formContext, | ||
| fieldComponents: { | ||
| ...fieldComponents, | ||
| ...extension.fieldComponents, | ||
| } as TComponents & TNewField, | ||
| formComponents: { | ||
| ...formComponents, | ||
| ...extension.formComponents, | ||
| } as TFormComponents & TNewForm, |
There was a problem hiding this comment.
Reserve core form/field API names in extendForm.
The new check only rejects keys that already exist in the parent component maps. Extensions can still use names that already exist on the runtime APIs themselves, e.g. AppForm, AppField, Field, Subscribe, or field members like handleChange. Those names are later merged onto the live form/field objects, so an extension can silently replace core behavior.
Suggested fix
function extendForm<
const TNewField extends Record<string, ComponentType<any>> & {
- [K in keyof TComponents]?: 'Error: field component names must be unique — this key already exists in the base form'
+ [K in keyof TComponents | keyof AnyFieldApi]?: 'Error: field component names must be unique and must not shadow field API members'
},
const TNewForm extends Record<string, ComponentType<any>> & {
- [K in keyof TFormComponents]?: 'Error: form component names must be unique — this key already exists in the base form'
+ [K in keyof TFormComponents | keyof AnyFormApi | 'AppField' | 'AppForm']?: 'Error: form component names must be unique and must not shadow form API members'
},
>(extension: { fieldComponents?: TNewField; formComponents?: TNewForm }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react-form/src/createFormHook.tsx` around lines 599 - 617,
extendForm currently only prevents extending keys that exist in the parent maps
but doesn't block names that collide with core runtime APIs (e.g., AppForm,
AppField, Field, Subscribe, handleChange) and thus can silently override core
behavior; modify extendForm to declare a reservedNames set (include core
exported API and runtime member names) and validate extension.fieldComponents
and extension.formComponents against that set, rejecting/throwing with a clear
error if any key from TNewField or TNewForm appears in reservedNames; reference
the extendForm function and the fieldComponents/formComponents merge locations
so the checks run before calling createFormHook and before the cast to
TComponents & TNewField / TFormComponents & TNewForm.
Summary by CodeRabbit
Release Notes
New Features
extendForm()method to enable extending custom forms with additional field and form components.Documentation