Skip to content

feat(react-from): extend appform#2106

Open
harry-whorlow wants to merge 6 commits intomainfrom
extend-appform
Open

feat(react-from): extend appform#2106
harry-whorlow wants to merge 6 commits intomainfrom
extend-appform

Conversation

@harry-whorlow
Copy link
Copy Markdown
Contributor

@harry-whorlow harry-whorlow commented Apr 2, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added extendForm() method to enable extending custom forms with additional field and form components.
    • Introduced a new React form composition example project.
  • Documentation

    • Enhanced form composition guide with clarity on extending custom forms and improved example accuracy.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

This pull request introduces form composition capabilities to TanStack React Form. It adds an extendForm method to the form hook system, enabling downstream teams to extend base appForms with custom components. The changes include updated documentation, a complete composition example project, and core library enhancements with comprehensive tests.

Changes

Cohort / File(s) Summary
Documentation
docs/framework/react/guides/form-composition.md
Corrected example field name reference and added a new section documenting how to extend custom appForms using extendForm(...) with examples of downstream-exclusive field components.
Example Project Configuration
examples/react/composition/{.eslintrc.cjs, .gitignore, tsconfig.json, package.json, index.html, README.md}
Added complete project scaffold with ESLint/TypeScript configuration, environment setup, build/dev scripts, dependencies, and HTML entrypoint for the composition example.
Example Project Source Code
examples/react/composition/src/{AppForm/AppForm.tsx, AppForm/FieldComponents/TextField.tsx, AppForm/FormComponents/SubmitButton.tsx, index.tsx}
Implemented working composition example with form hook context setup, reusable field/form components (TextField, SubmitButton), and a sample App demonstrating form validation and submission patterns.
Core Library - Form Extension
packages/react-form/src/createFormHook.tsx
Added extendForm() method to createFormHook return value, enabling merging of field/form components with compile-time uniqueness checking via generic constraints to prevent key collisions.
Core Library - Tests
packages/react-form/tests/{createFormHook.test-d.tsx, createFormHook.test.tsx}
Added comprehensive type-level and runtime tests validating extendForm behavior: component merging, chaining extensions, component availability across hook contexts, and error assertions for duplicate names.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A form that bends, extends with grace,
Platform gifts a solid base,
Downstream teams add their own flair,
Components merge beyond compare!
Hops with joy

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title contains a typo ('react-from' instead of 'react-form') and lacks specificity about what is being extended. Correct the typo to 'feat(react-form): extend appform' or provide more specific details about the extension capability being added.
Description check ⚠️ Warning No pull request description was provided; the template requires sections for changes, checklist, and release impact. Add a detailed description following the provided template, explaining the feature and confirming the checklist items and changeset status.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch extend-appform

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Apr 2, 2026

View your CI Pipeline Execution ↗ for commit 23d9e03

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 58s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 12s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-02 22:46:37 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

🚀 Changeset Version Preview

1 package(s) bumped directly, 12 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/svelte-form 1.28.6 → 1.28.7 Changeset
@tanstack/angular-form 1.28.6 → 1.28.7 Dependent
@tanstack/form-core 1.28.6 → 1.28.7 Dependent
@tanstack/form-devtools 0.2.20 → 0.2.21 Dependent
@tanstack/lit-form 1.23.26 → 1.23.27 Dependent
@tanstack/react-form 1.28.6 → 1.28.7 Dependent
@tanstack/react-form-devtools 0.2.20 → 0.2.21 Dependent
@tanstack/react-form-nextjs 1.28.6 → 1.28.7 Dependent
@tanstack/react-form-remix 1.28.6 → 1.28.7 Dependent
@tanstack/react-form-start 1.28.6 → 1.28.7 Dependent
@tanstack/solid-form 1.28.6 → 1.28.7 Dependent
@tanstack/solid-form-devtools 0.2.20 → 0.2.21 Dependent
@tanstack/vue-form 1.28.6 → 1.28.7 Dependent

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2106

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2106

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2106

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2106

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2106

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2106

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2106

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2106

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2106

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2106

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2106

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2106

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2106

commit: 23d9e03

@sentry
Copy link
Copy Markdown

sentry bot commented Apr 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.25%. Comparing base (6892ed0) to head (23d9e03).
⚠️ Report is 162 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@harry-whorlow harry-whorlow marked this pull request as ready for review April 6, 2026 19:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
packages/react-form/tests/createFormHook.test-d.tsx (1)

993-1021: The chaining type test never proves ThirdField survived the second extension.

This case creates ThirdField, but the assertions still only cover Test and ExtendedField, then stop at expectTypeOf(doublyExtendedForm.AppField).toBeFunction(). A regression where the second extendForm drops 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 returns false instead of undefined when valid.

When value.includes('error') is false, the && short-circuit returns false. While TanStack Form may handle this, explicitly returning undefined for 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 exporting extendForm for downstream composition.

Given this PR introduces form composition capabilities via extendForm, you might want to export it alongside useAppForm so 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: TextField can 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4276176 and 23d9e03.

⛔ Files ignored due to path filters (2)
  • examples/react/composition/public/emblem-light.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • docs/framework/react/guides/form-composition.md
  • examples/react/composition/.eslintrc.cjs
  • examples/react/composition/.gitignore
  • examples/react/composition/README.md
  • examples/react/composition/index.html
  • examples/react/composition/package.json
  • examples/react/composition/src/AppForm/AppForm.tsx
  • examples/react/composition/src/AppForm/FieldComponents/TextField.tsx
  • examples/react/composition/src/AppForm/FormComponents/SubmitButton.tsx
  • examples/react/composition/src/index.tsx
  • examples/react/composition/tsconfig.json
  • packages/react-form/src/createFormHook.tsx
  • packages/react-form/tests/createFormHook.test-d.tsx
  • packages/react-form/tests/createFormHook.test.tsx

Comment on lines +209 to +220
```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 },
})
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

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.

Comment on lines +9 to +12
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
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

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.

Suggested change
<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.

Comment on lines +50 to +52
>
{(f) => <f.TextField label="last name" />}
</form.AppField>
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

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.

Suggested change
>
{(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).

Comment on lines +599 to +617
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,
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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant