VOOZH about

URL: https://dev.to/kensaadi/stop-writing-form-glue-code-mui-components-already-connected-to-react-hook-form-3k1a

⇱ Stop Writing Form Glue Code: MUI Components Already Connected to React Hook Form - DEV Community


If you've used React Hook Form with Material UI, you know the pattern.

It works well.
It's flexible.
But it's also… repetitive.

You're not really building forms.

You're wiring things together.

The Pattern We All Write

A simple input with RHF + MUI usually looks like this:

import { Controller } from 'react-hook-form';
import { TextField } from '@mui/material';

<Controller
 name="email"
 control={control}
 rules={{ required: 'Email is required' }}
 render={({ field, fieldState }) => (
 <TextField
 {...field}
 label="Email"
 error={!!fieldState.error}
 helperText={fieldState.error?.message}
 />
 )}
/>

Nothing wrong here.

But now multiply that by:

  • 5 fields
  • 10 fields
  • 20 fields

And you start noticing something:

You're repeating the same integration logic over and over again.


The Real Issue: Integration, Not the Libraries

Material UI gives you great UI components.
React Hook Form gives you great form state.

But neither of them knows about the other.

So every time, you write the glue:

  • connect value / onChange
  • map errors
  • pass validation rules
  • keep UI consistent

That glue becomes your form.


What If Components Already Knew RHF?

Instead of connecting MUI to RHF manually…

What if your components were already connected?

That's what dashforge-ui does.

A TextField from dashforge-ui is still visually a MUI component — but it already understands React Hook Form.

So instead of this:

<Controller
 name="email"
 control={control}
 rules={{ required: 'Email is required' }}
 render={({ field, fieldState }) => (
 <TextField {...field} error={!!fieldState.error} />
 )}
/>

You write this:

import { TextField } from '@dashforge/ui';

<TextField
 name="email"
 label="Email"
 rules={{ required: 'Email is required' }}
/>

Same behavior.
Same validation.
Same form state.

But no glue code.


Important: RHF Is Still There

This is not replacing React Hook Form.

It's built on top of it.

  • form state → handled by RHF
  • validation → handled by RHF
  • performance → same RHF behavior

You're just not wiring it manually anymore.

Think of it as RHF + MUI, already connected.


What Changes in a Real Form

Before (RHF + MUI Vanilla)

import { useForm, Controller } from 'react-hook-form';
import { TextField, Button, Box } from '@mui/material';

interface LoginForm {
 email: string;
 password: string;
}

export function LoginForm() {
 const { control, handleSubmit, formState: { errors } } = useForm<LoginForm>({
 defaultValues: { email: '', password: '' },
 });

 return (
 <Box component="form" onSubmit={handleSubmit((data) => console.log(data))}>
 <Controller
 name="email"
 control={control}
 rules={{ required: 'Email is required' }}
 render={({ field, fieldState: { error } }) => (
 <TextField
 {...field}
 label="Email"
 error={!!error}
 helperText={error?.message}
 margin="normal"
 fullWidth
 />
 )}
 />

 <Controller
 name="password"
 control={control}
 rules={{ required: 'Password is required' }}
 render={({ field, fieldState: { error } }) => (
 <TextField
 {...field}
 label="Password"
 type="password"
 error={!!error}
 helperText={error?.message}
 margin="normal"
 fullWidth
 />
 )}
 />

 <Button type="submit" variant="contained" fullWidth sx={{ mt: 2 }}>
 Login
 </Button>
 </Box>
 );
}

That's 52 lines for 2 fields.

After (dashforge-ui)

import { DashForm, TextField, Button } from '@dashforge/ui';

type LoginForm = {
 email: string;
 password: string;
};

export function LoginForm() {
 return (
 <DashForm<LoginForm>
 defaultValues={{ email: '', password: '' }}
 onSubmit={(values) => console.log(values)}
 >
 <TextField
 name="email"
 label="Email"
 rules={{ required: 'Email is required' }}
 />

 <TextField
 name="password"
 label="Password"
 type="password"
 rules={{ required: 'Password is required' }}
 />

 <Button type="submit">
 Login
 </Button>
 </DashForm>
 );
}

That's 22 lines. 58% less code.

No Controller.
No manual mapping.
No repetition.


Handling Real Complexity

Because it still uses RHF under the hood, you keep all the power.

Conditional Fields

<TextField
 name="company"
 label="Company"
 visibleWhen={(engine) => {
 const accountType = engine.getNode('accountType')?.value;
 return accountType === 'business';
 }}
/>

Cross-Field Validation

<TextField
 name="confirmPassword"
 label="Confirm Password"
 rules={{
 validate: (value, values) =>
 value === values.password || 'Passwords do not match',
 }}
/>

Async Validation

<TextField
 name="email"
 label="Email"
 rules={{
 validate: async (value) => {
 const isAvailable = await checkEmailAvailability(value);
 return isAvailable ? true : 'Email already registered';
 },
 }}
/>

Same flexibility.
Less boilerplate.


Performance & Bundle Size Impact

  • React Hook Form: ~8.5kB (gzipped)
  • MUI TextField + Button: ~20kB (gzipped)
  • dashforge-ui core: ~12kB (gzipped)

Total with dashforge-ui: ~40.5kB (vs ~65kB with manual RHF + MUI setup)

The integration is actually lighter because dashforge-ui eliminates the Controller overhead and consolidates common patterns.


Developer Experience: Before vs After

Task RHF + MUI dashforge-ui
Add a required text field 15 lines (Controller wrapper) 4 lines
Add email validation 5 lines (rules + helperText mapping) 1 rule object
Add conditional visibility ~20 lines (JSX conditional) 1 visibleWhen function
Type your form data Manual interface + wiring Inferred from form props
Error handling Manual fieldState mapping Built-in, automatic
Form submission Manual handleSubmit + wiring Built-in onSubmit

When to Use dashforge-ui

✅ Perfect For:

  • CRUD forms (create/edit resources)
  • Medium to large forms (8+ fields)
  • Multi-step forms with conditional logic
  • Rapid prototyping where speed matters
  • Teams wanting form consistency across projects
  • React + TypeScript shops that value developer experience

⚠️ Consider alternatives if:

  • You need completely custom field rendering everywhere (dashforge-ui supports this, but with more config)
  • Your form is a single-field micro-interaction (vanilla RHF + MUI might be simpler)
  • You need drag-and-drop form builders or visual design tools

Why This Matters

This is not about saving a few lines of code.

It's about removing an entire category of repetitive work:

  • no more Controller wrappers
  • no more error plumbing
  • no more field wiring

You focus on:

  • the structure of your form
  • the logic between fields
  • the user experience

Getting Started

npm install @dashforge/ui @dashforge/forms @dashforge/ui-core react-hook-form @mui/material @emotion/react @emotion/styled

Full documentation: dashforge-ui.com

Minimal Example

import { DashForm, TextField, Button } from '@dashforge/ui';

export function ContactForm() {
 return (
 <DashForm
 onSubmit={(values) => {
 fetch('/api/contact', {
 method: 'POST',
 body: JSON.stringify(values),
 });
 }}
 >
 <TextField
 name="name"
 label="Name"
 rules={{ required: 'Name is required' }}
 />

 <TextField
 name="email"
 label="Email"
 type="email"
 rules={{
 required: 'Email is required',
 pattern: { value: /^\S+@\S+$/, message: 'Invalid email' },
 }}
 />

 <TextField
 name="message"
 label="Message"
 multiline
 rows={4}
 rules={{ required: 'Message is required' }}
 />

 <Button type="submit">
 Send
 </Button>
 </DashForm>
 );
}

Fully typed. Validated. Styled with MUI. No wiring required.


Final Thought

React Hook Form is great.
Material UI is great.

But the integration between them?

That's where most of the friction lives.

dashforge-ui removes that friction — by giving you components that already understand both.

No layer on top. No new paradigm to learn. Just RHF + MUI, finally connected at the framework level.


Beyond Simple Forms: Managing Field Dependencies

Real-world forms have dependencies between fields.

Examples:

  • Show a "Shipping Address" section only if the user selects "Ship to different address"
  • Validate a "Confirm Password" field against the password value
  • Populate a "City" dropdown based on the selected "Country"
  • Disable a "Submit" button if dependent validations haven't passed

With vanilla RHF + MUI, these become spaghetti code.

How dashforge-ui Handles Dependencies

Field visibility is straightforward with visibleWhen:

<TextField
 name="country"
 label="Country"
 rules={{ required: true }}
/>

{/* Only renders if country === 'US' */}
<TextField
 name="state"
 label="State"
 visibleWhen={(engine) => engine.getNode('country')?.value === 'US'}
/>

For more complex logic — conditional validation rules, async operations triggered by field changes, dynamic options loading — dashforge-ui uses Reactions.

A Reaction is simple:

const reactions = [
 {
 id: 'load-cities-on-country-change',
 watch: ['country'], // Watch for country changes
 when: (ctx) => Boolean(ctx.getValue('country')), // Only run if country has a value
 run: async (ctx) => {
 const country = ctx.getValue<string>('country');
 const cities = await fetchCities(country);
 // Update runtime state (not form values)
 ctx.setRuntime('city', { options: cities });
 }
 }
];

<DashForm reactions={reactions}>
 {/* fields here */}
</DashForm>

Think of Reactions as "When this happens, do that" — they watch field changes and run side effects.

This is the real power behind complex form dependencies: instead of manually wiring useWatch + useEffect chains, you describe what triggers what at the form level.

Want to go deeper? I've written a detailed guide on this exact problem:

📖 Building Dependent Form Fields in React: A Practical Approach

That guide covers dependency patterns, performance implications, and real-world solutions. dashforge-ui integrates those patterns into the component model.


👉 If you're using React Hook Form today, I'm curious: how much of your form code is actual logic vs wiring?

Learn more: dashforge-ui.com

GitHub: github.com/dashforge-ui

Deep dive on dependencies: Building Dependent Form Fields in React