Skip to content

Upgrade react-hook-form and use new form-level validate#3188

Open
david-crespo wants to merge 1 commit intomainfrom
rhf-validate-prop
Open

Upgrade react-hook-form and use new form-level validate#3188
david-crespo wants to merge 1 commit intomainfrom
rhf-validate-prop

Conversation

@david-crespo
Copy link
Copy Markdown
Collaborator

Resolvers are really for transforming form input into another shape. We were using it for form-level cross-field validation. The new form-level validate API added in react-hook-form/react-hook-form#13195 is perfect for what we were doing. It does not override existing validations and it runs whenever validations are supposed to run (I think resolver only runs on submit.

Interesting changes in RHF

I had Claude go through the releases since the old version and summarize the interesting stuff.

Features worth a look

  • v7.69.0 reset({ keepIsValid: true }) fix — the existing keepIsValid option was buggy before. We don't currently use it but SideModalForm's "name already exists" flow uses setError in a post-submit effect; switching to errors prop with focus (v7.57.0 focus form field for errors supplied by errors prop) could be cleaner.
  • v7.65.0 <Watch /> + v7.68.0 <FormStateSubscribe /> — render-prop components that re-render only when a named field (or formState slice) changes. instance-create.tsx and firewall-rules-common.tsx have lots of useWatch at the top level; some of that churn could be isolated inside a <Watch> render-prop. Worth trying only if profiling shows the re-renders hurt.
  • v7.61.0 useWatch({ compute }) — subscribe to the whole form but only surface a derived value when a condition matches. Could collapse useWatch + useMemo pairs. instance-create.tsx:451-463 computes bootDiskSource from four useWatch calls; a single useWatch({ compute }) would work.
  • v7.63.0 getValues(undefined, { dirtyFields: true }) — extract only dirty fields. Useful for PATCH-style edits; our edit forms mostly send the full object so the win is small.
  • v7.55.0 createFormControl / subscribe — subscribe to form state outside React (e.g., from a store). Not obviously useful here.
  • v7.56.0 reactive mode / reValidateMode — these become reactive to re-renders. The firewall-rules HACK comment at firewall-rules-common.tsx:136-143 is specifically about the validate/reValidate regime swap, but that HACK is about isSubmitted state after reset, not about mode being non-reactive, so this doesn't directly help.

Bug that might have been biting us

  • v7.72.1setValue with shouldDirty no longer pollutes unrelated dirty fields. We use setValue heavily (silo-create, idp-create, instance-create); if any of those use shouldDirty we likely had ghost-dirty fields.
    • This sounds really familiar...........
  • v7.66.1deepEqual uses Object.is for NaN. subnet-pool-member-add stores NaN as the unset state for optional NumberFields — pre-fix, isDirty might have flipped on those.
  • v7.69.0 — race between setError and setFocus resolved. Relevant to our SideModalForm effect that calls both.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
console Ready Ready Preview Apr 17, 2026 11:01pm

Request Review

* Validates IP range addresses against the pool's IP version.
* Ensures both addresses are valid IPs and match the pool's version.
*/
function createResolver(poolVersion: IpVersion) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I believe this was originally a resolver because pools were not locked to a single IP version, so we had to make sure the start and end were of the same version. Now we can just compare each one individually to the version of the pool. So this can all just be a field-level validation. This change does not rely on form-level validation, this was just discovered while looking for candidates for form-level validation.

// Uses form-level validate (RHF ≥7.72.0) so we can look at all three fields
// together. Unlike `resolver`, this runs alongside field-level validation, so
// `required` / `min` / `max` on the fields still apply.
export function validateForm(poolVersion: IpVersion, values: MemberAddForm) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Look at this with whitespace changes hidden to see what a small change it is. Basically it's just the return type changing.

https://github.com/oxidecomputer/console/pull/3188/changes?w=1

const subnetWidth = parsed.type !== 'error' ? parsed.width : undefined
const inRange = (v: number) => !Number.isNaN(v) && v >= 0 && v <= maxBound

const errors: Partial<Record<keyof MemberAddForm, { type: string; message: string }>> = {}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Don't love this, will revisit.

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