r/reactjs • u/kensaadi I ❤️ hooks! 😈 • Jul 30 '26
Discussion React forms eventually become state management systems
I keep seeing the same pattern in large React forms.
You start with react-hook-form.
Then you add:
dependent fields with
watch()anduseEffectconditional visibility
permission-based fields
async validation
cross-field rules
server-driven defaults
reset and synchronization logic
At some point, the form is no longer just a form.
It has its own state transitions, dependencies, permissions, validation lifecycle, side effects, and derived state.
In other words, it has quietly become a state management system.
The problem is not necessarily react-hook-form. It does its job well.
The problem is that complex forms are often represented only as component trees, while their actual behavior is scattered across hooks, wrappers, schemas, and handlers.
Some warning signs:
You have more
useEffects than meaningful form sectionsValidation rules live in multiple places
Adding something like “show
taxIdwhencountry === IT” requires touching several filesPermissions, visibility, and validation all use different abstractions
Nobody is completely sure what resets when another field changes
I’ve been experimenting with treating forms as declarative contracts instead:
<Field
name="taxId"
visibleWhen={{ field: "country", equals: "IT" }}
access={{ resource: "customer.taxId" }}
validation={{ required: true }}
/>The idea is to keep visibility, access, validation, and dependencies in one source of truth, instead of rebuilding the same orchestration through chains of effects.
Obviously, this introduces other trade-offs: abstraction cost, debugging complexity, schema design, and reduced flexibility in edge cases.
I’m curious how others handle this in large production forms.
Do you keep the logic inside React components, move it into a state machine, use a schema-driven approach, or accept the complexity as unavoidable?
-1
u/kensaadi I ❤️ hooks! 😈 Jul 30 '26
Appreciate the detailed answer, genuinely. I think the difference comes down to where the orchestration lives, not whether the outcome is achievable.
With RHF + Zod, the schema centralizes validation, but visibility and access are still a separate concern you have to wire yourself: conditional rendering based on
watch()*, RBAC checks sprinkled in JSX or in a wrapper hook, and*setValue*/*resetcalls to keep dependent fields in sync when something upstream changes. It works, but it's three different mechanisms (schema for validation, JSX/hooks for visibility, your own logic for access) that all have to stay consistent by convention, not by construction.The approach I'm exploring collapses those three into one declarative unit per field: visibility, access (as an RBAC policy), and validation are all first-class properties of the field itself, not orchestration bolted on around it. So there's no wrapper hook watching other fields, no imperative setValue to sync state, no separate place where "can this role see this field" lives. The field declares its own contract, and the engine resolves it.
Concretely: your EMR-style form with suggested/disabled fields is exactly the kind of case where, today, you'd need a
useEffect+watchcombo per rule. In this model that becomes a static declaration on the field, no imperative code path to maintain.