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
Top comments (0)