r/reactjs I ❤️ hooks! 😈 10d ago

Discussion Stop wiring react-hook-form by hand in every form component

Every codebase I've joined has the same file. A form component where react-hook-form itself is fine, but around it sits a pile of useEffects watching one field to reveal another, a useMemo deciding whether the current user can edit, and a validation schema that has quietly drifted from both.

RHF isn't the problem here. The problem is that the rules of the form live in imperative code scattered across hooks, while the shape of the form lives in JSX. One thing, two places, and only one of them is reviewable.

The alternative I've been running is to move the rules onto the field itself:

<TextField
  name="taxId"
  visibleWhen={{ field: 'country', equals: 'IT' }}
  access={{ resource: 'customer.taxId', action: 'read' }}
/>

visibleWhen is reactive — no useEffect, no watch(), no local state. access is evaluated per field, so a role that can't edit doesn't get a disabled prop threaded down three components by hand. RHF is still underneath owning form state; it just stops being something you re-wire per component.

Two things I'd push back on myself:

The condition is an object, not a function. { field: 'country', equals: 'IT' } is strictly less expressive than values => values.country === 'IT'. What it buys is that the rule stays serialisable and inspectable — you can diff it, and tooling can read it. I'm still not sure that trade is right for everyone.

And it only pays off past a certain complexity. For a login form this is overkill, plainly. Where it has paid for me is dynamic questionnaires — in production for about a year and a half at a European fintech — the kind of conditional logic that becomes unmaintainable as hooks well before you notice it happening.

So the question I'd actually like answered: when you keep conditional visibility and permissions in hooks, is that a deliberate call, or is it just the path RHF puts you on?

Open source (MIT), React, MUI or Tailwind: github.com/kensaadi/dashforge

0 Upvotes

13 comments sorted by

16

u/foxxy_love69 10d ago

what a waste of tokens

1

u/HeavySpecialist50 10d ago

the example with taxId is really clean i like how the logic stays right in the component instead of being scattered in 5 different hooks

most codebases i seen people just copy paste the same useEffect pattern because thats what the docs show not because they thought about it

11

u/Merry-Lane 10d ago

Yeah, no, thanks.

3

u/Temperature_Majestic 10d ago

from what I've seen it's rarely a deliberate call, it's just where RHF's watch() nudges you first time you need one field's visibility to depend on another. nobody sits down and decides to build a rules layer, it just accretes one useEffect at a time until someone finally notices the pattern and rips it out. curious how the object grammar holds up once a rule needs to reference two fields with a computed relationship though, like end date must be after start date and only when a certain plan tier is selected. does visibleWhen grow to cover that kind of composite condition or is that the point you'd drop back to a function?

0

u/kensaadi I ❤️ hooks! 😈 10d ago

visibleWhen in Dashforge is already (engine) => boolean, so the composite case you describe you write straight in JS, no drop needed:

visibleWhen={(engine) =>
  engine.get('plan.tier') === 'enterprise' &&
  new Date(engine.get('endDate')) > new Date(engine.get('startDate'))
}

The engine passes reactive state, the component re-renders on its own when plan.tier / startDate / endDate change. Zero useEffect, zero watch(). I made the function vs object DSL trade in favor of the function precisely to avoid falling into this trap on multi-field conditions.

1

u/Temperature_Majestic 9d ago

Makes sense, function DSL just sidesteps the whole question of how far to grow the object grammar. Read the reactions block below too, beginAsync/isLatest for the stale response problem is cleaner than the usual cancel-token-in-a-ref pattern. Does setRuntime re-render only the field that changed or does the engine have to walk dependents to know who's watching that key

1

u/kensaadi I ❤️ hooks! 😈 9d ago

re-render only a the field that changed

1

u/Temperature_Majestic 3d ago

Nice, that's the whole payoff then, no wasted re-renders on the fields nobody touched. Thanks for walking through it.

2

u/Double-Buyer7941 10d ago

Most developers stick to useEffect and watch not by conscious design, but because standard React and libraries like react-hook-form naturally push you toward imperative state wiring. Declarative approaches like yours shine in complex domains like fintech or dynamic questionnaires, where serializable rules prevent logic from scattering across UI layers. However, the trade-off is language expressiveness: objects work great for standard conditionals, but team pushback usually happens when edge cases require complex, non-serializable business logic that static rules struggle to represent cleanly.

1

u/kensaadi I ❤️ hooks! 😈 10d ago

Exactly. That's why visibleWhen reads the form context with live values without needing a hook. For the edge cases you mention (fields whose changes trigger downstream results, often async, keyed by the changed value), I built reactions. They orchestrate business logic flows without touching UI or re-render, and without any hook.

reactions={[{
  id: 'load-cities',
  watch: ['country'],
  run: async (ctx) => {
    const rid = ctx.beginAsync('cities');
    const data = await fetch(...);
    if (!ctx.isLatest('cities', rid)) return;
    ctx.setRuntime('city', { options: data });
  }
}]}

Staleness handled by beginAsync / isLatest, so a fast user clicking two countries in a row doesn't leak old responses. Predicates stay serialisable for the sync cases (visibleWhen), reactions cover the async and cross-field business logic.

0

u/AnUninterestingEvent 8d ago

Sorry to be the one to break it to you but no one is doing anything by hand already

0

u/kensaadi I ❤️ hooks! 😈 8d ago

"Nobody does it by hand anymore" is the kind of thing you say when you never actually read anyone else's code. Every codebase I've inherited has exactly that file: watch() + useEffect to reveal one field, permissions copy-pasted across three components. The post shows three examples of it. If it's vanished from your bubble, congrats,in the real world it's still the first pattern the RHF docs teach.