Sync to v7.72.0#99
Conversation
* feat: build in form valdiate * update max size * update with input react to validate form * update api * update api * fix type * fix all the format issues * update valdiation logic and add test * remove only * update validation logic to support event-based validation * make prop name shorter and update validation * fix tests and api contractor * rename form error to form * rename to valid * minor clean up * revert back function * fix with early exit * fix test * fix type and contract * update api * lint update
…eact-hook-form#13299) * 🐞 fix: prevent useFieldArray from marking unrelated fields as dirty (react-hook-form#13054) Previously, _setFieldArray replaced the entire dirtyFields object via getDirtyFields(), which included false entries for non-dirty fields. This caused unrelated fields (name, age, etc.) to appear in dirtyFields after append/remove operations. Now only the field array portion of dirtyFields is updated, preserving the dirty state of other fields. * 🐞 fix: use root field name for nested field array dirty tracking * 🐞 fix: use getNodeParentName for nested field array dirty tracking * ✅ test: remove test comments * ✅ test: add nested indexed field array regression test
This commit updates the react-hook-form V7 JavaScript and TypeScript CodeSandbox links in GitHub issue template. This will ensure that the latest version of `react-hook-form` is used.
|
Size Change: +1.61 kB (+2.36%) Total Size: 69.9 kB
|
There was a problem hiding this comment.
Pull request overview
Syncs this library’s React Hook Form–compatible API surface to upstream RHF v7.72.0, including new form-level validation and additional formState subscriptions, while adding regression tests around field array dirtyFields isolation.
Changes:
- Introduces form-level validation via a new
validateoption inuseFormand newerrors.form/form.*error paths. - Extends subscribable
formStatefields to includeisSubmittedandsubmitCount. - Adjusts dirtyFields updates for field array operations and adds/expands tests to ensure unrelated dirtyFields aren’t polluted.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/validator.ts | Adds ValidateForm/ValidateFormEventType and form validation result typing. |
| src/types/form.ts | Wires new validate prop into UseFormProps; adds isSubmitted/submitCount to ReadFormState; expands setError/clearErrors name types to include form. |
| src/types/errors.ts | Adds errors.form typing. |
| src/logic/createFormControl.ts | Implements form-level validation execution and error mapping; updates dirtyFields handling for field arrays. |
| src/logic/updateFieldArrayRootError.ts | Uses new ROOT_ERROR_TYPE constant for field-array root error key. |
| src/constants.ts | Adds additional event constants and new FORM_ERROR_TYPE/ROOT_ERROR_TYPE constants. |
| src/tests/** | Adds/updates tests for submit-state subscriptions, form-level validation behavior, and dirtyFields isolation. |
| reports/api-extractor.md.api.md | Updates API report; introduces new API Extractor warning around EVENTS. |
| package.json | Bumps package version to 7.72.0. |
| .github/ISSUE_TEMPLATE/bug_report.yml | Adds CodeSandbox template links to the bug report template. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import type { EVENTS, INPUT_VALIDATION_RULES } from '../constants'; | ||
|
|
||
| import type { Message } from './errors'; | ||
| import type { FieldValues } from './fields'; | ||
| import type { Mode } from './form'; | ||
| import type { FormState, Mode } from './form'; |
There was a problem hiding this comment.
ValidateFormEventType / InputValidationRules use typeof EVENTS / typeof INPUT_VALIDATION_RULES, but those are imported with import type. Type-only imports can’t be referenced in a typeof type query, so this should fail TS compilation. Use a value import (import { EVENTS, INPUT_VALIDATION_RULES }) or refactor the types to not depend on the runtime constants.
| > & { | ||
| root?: Record<string, GlobalError> & GlobalError; | ||
| form?: GlobalError; | ||
| }; |
There was a problem hiding this comment.
UseFormSetError/UseFormClearErrors now allow 'form' and form.${string}, and createFormControl sets errors at paths like form.${key}. But FieldErrors types form?: GlobalError, which doesn’t support nested keys. Either change form to a nested map type (similar to root?: Record<string, GlobalError> & GlobalError) or remove support for form.${string} so the public types match runtime behavior.
| setError(`${FORM_ERROR_TYPE}.${key}`, { | ||
| message: isString(result.message) ? result.message : '', | ||
| type: INPUT_VALIDATION_RULES.validate, |
There was a problem hiding this comment.
In the object-result branch, message: isString(result.message) ? result.message : '' reads from result.message, but result is the whole map (key -> error), so this will always be '' and ignore the per-key error details. It also hardcodes type instead of using the returned error.type. Use the error variable (result[key]) for message/type (and handle when it’s missing).
| setError(`${FORM_ERROR_TYPE}.${key}`, { | |
| message: isString(result.message) ? result.message : '', | |
| type: INPUT_VALIDATION_RULES.validate, | |
| const message = isObject(error) | |
| ? (isString((error as any).message) ? (error as any).message : '') | |
| : isString(error) | |
| ? error | |
| : ''; | |
| const type = | |
| isObject(error) && isString((error as any).type) | |
| ? (error as any).type | |
| : INPUT_VALIDATION_RULES.validate; | |
| setError(`${FORM_ERROR_TYPE}.${key}`, { | |
| message, | |
| type, |
| if (props.validate) { | ||
| context.runRootValidation = true; | ||
| const result = await validateForm({ | ||
| name, | ||
| eventType, | ||
| }); | ||
|
|
||
| if (!result) { | ||
| context.valid = false; | ||
|
|
||
| if (onlyCheckValid) { | ||
| return context.valid; | ||
| } | ||
| } |
There was a problem hiding this comment.
executeBuiltInValidation marks the form invalid only when !result, but props.validate can return a string (error message) or an object (per-field errors), both of which are truthy. That means _formState.isValid can remain true while form-level errors have been set. The validity check should treat string/object-with-errors as invalid (and only true as valid).
| const executeBuiltInValidation = async ({ | ||
| fields, | ||
| onlyCheckValid, | ||
| name, | ||
| eventType, | ||
| context = { | ||
| valid: true, | ||
| runRootValidation: false, | ||
| }, | ||
| ) => { | ||
| }: { | ||
| fields: FieldRefs; | ||
| onlyCheckValid?: boolean; | ||
| name?: FieldPath<TFieldValues> | FieldPath<TFieldValues>[]; | ||
| eventType: ValidateFormEventType; | ||
| context?: { | ||
| valid: boolean; | ||
| runRootValidation?: boolean; | ||
| }; | ||
| }) => { | ||
| if (props.validate) { | ||
| context.runRootValidation = true; | ||
| const result = await validateForm({ | ||
| name, | ||
| eventType, | ||
| }); | ||
|
|
There was a problem hiding this comment.
executeBuiltInValidation calls validateForm at the start whenever props.validate exists, and then recursively calls itself for nested field objects. Because context.runRootValidation is never checked, form-level validation can run many times during a single validation pass (once per recursion), which is both expensive and can lead to repeated/stale error updates. Consider guarding with if (props.validate && !context.runRootValidation) (or similar) so form-level validation runs only once per pass.
| // Warning: (ae-forgotten-export) The symbol "EVENTS" needs to be exported by the entry point index.d.ts | ||
| // | ||
| // @public (undocumented) | ||
| export type ValidateFormEventType = (typeof EVENTS)[keyof typeof EVENTS]; |
There was a problem hiding this comment.
API Extractor is now emitting an (ae-forgotten-export) warning for EVENTS because ValidateFormEventType references it. This usually breaks the API report / CI gate. Either export EVENTS from the public entrypoint (src/index.ts) or change ValidateFormEventType to not depend on the EVENTS value (e.g., a literal union), so the public d.ts can be generated without forgotten-export warnings.
| // Warning: (ae-forgotten-export) The symbol "EVENTS" needs to be exported by the entry point index.d.ts | |
| // | |
| // @public (undocumented) | |
| export type ValidateFormEventType = (typeof EVENTS)[keyof typeof EVENTS]; | |
| // @public (undocumented) | |
| export type ValidateFormEventType = string; |
| if (isObject(result)) { | ||
| for (const key in result) { | ||
| const error = result[key]; | ||
|
|
||
| if (error) { | ||
| setError(`${FORM_ERROR_TYPE}.${key}`, { | ||
| message: isString(result.message) ? result.message : '', | ||
| type: INPUT_VALIDATION_RULES.validate, | ||
| }); | ||
| } | ||
| } | ||
| } else if (isString(result) || !result) { |
There was a problem hiding this comment.
When props.validate returns an object, validateForm only sets errors for keys present/truthy, but it never clears existing form.* errors when the object is empty or when a previously-returned key disappears. This can leave stale form-level errors in state; consider clearing FORM_ERROR_TYPE before applying the new per-key errors (or explicitly unsetting keys not returned).
Resolves #98
Summary
validateoption inuseForm)