Browse docs
Browse docs
Because even simple firm require orchestration code. React Hook Form and MUI are powerful libraries, but they solve different problems.
The integration layer between them is still your responsability.
In practice, the means writing Controller wrappers, mapping error state manually, handling touched fields, wiring validation
messages, and coordinating field dependencies yourself.
As forms become dynamic: conditional sections, corss-field validation, dependent fields, the amount of orchestration code grows quickly.
When forms need conditional fields and dynamic behavior, glue code appears.
React Hook Form handles registration and validation. MUI provides the components. But as soon as you need conditional fields, cross-field validation, or dynamic error handling, you write Controller wrappers, scatter watch() calls across components, and manually wire error state. The glue code accumulates.
Support form with conditional field: show details when bug is selected. The same form — two very different approaches.
MUI + React Hook Form:
function SupportForm() {
const { control, watch } = useForm();
const category = watch('category');
return (
<form>
<Controller
name="category"
control={control}
rules={{ required: 'Category is required' }}
render={({ field, fieldState }) => (
<Select
{...field}
error={!!fieldState.error}
>
<MenuItem value="bug">Bug</MenuItem>
<MenuItem value="feature">Feature</MenuItem>
<MenuItem value="billing">Billing</MenuItem>
</Select>
)}
/>
{category === 'bug' && (
<Controller
name="details"
control={control}
rules={{
validate: (value) =>
value?.trim()
? true
: 'Details required for bugs'
}}
render={({ field, fieldState }) => (
<TextField
{...field}
label="Bug Details"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
)}
</form>
);
}Controller + watch + conditional JSX + manual error wiring
Dashforge:
function SupportForm() {
return (
<DashForm onSubmit={handleSubmit}>
<Select
name="category"
label="Category"
options={[
{ value: 'bug', label: 'Bug' },
{ value: 'feature', label: 'Feature' },
{ value: 'billing', label: 'Billing' },
]}
rules={{ required: 'Category is required' }}
/>
<TextField
name="details"
label="Bug Details"
visibleWhen={(engine) =>
engine.getNode('category')?.value === 'bug'
}
rules={{
validate: (value) =>
value?.trim()
? true
: 'Details required for bugs'
}}
/>
</DashForm>
);
}No Controller. No watch. Just declarative fields.
The difference compounds as forms grow.
A simple login form has minimal glue code.
But forms with 10+ fields, conditional sections, cross-field validation, and dynamic behavior become orchestration nightmares.
Dashforge moves that orchestration into the framework. Validation, visibility, and error handling stay close to the field. As forms scale, your code stays clean.
Three practical improvements:
No Controller. Ever. Fields register themselves. Errors bind automatically. Touched state handled internally. Write the field once — not wrapped in render props.
Show/hide fields declaratively. Use visibleWhen on the field instead of watch() + conditional JSX in the parent. The dependency lives where it belongs.
Validation stays with the field. Cross-field rules access form state without manual wiring. Validation logic lives next to the field it validates.
If this looks like a better way to structure forms, start by installing Dashforge.