Recently, I started working on building a dynamic form system using React + Formik where the entire form UI was driven by JSON configuration instead of hardcoded components.
Instead of manually writing fields like this:
I wanted the frontend to dynamically render forms based on configuration received from the backend.
This is the beginning of a blog series where I’ll cover:
- dynamic form rendering
- layout handling
- dynamic Yup validation schemas
- conditional child fields
- API-driven dropdowns
- multi-step forms
- nested field handling
Starting with the foundation: rendering forms dynamically from JSON.
🧠 Why Dynamic Forms?
Static forms work fine initially.
But as forms become:
- configurable
- reusable
- API-driven
- multi-step
hardcoding fields becomes difficult to maintain.
Dynamic forms help centralize:
- field structure
- UI layout
- validation metadata
- rendering logic
inside configuration itself.
📦 Form Configuration Structure
I started with a grouped schema structure like this:
export const formConfig = {
"personalDetails":[
{
"colWidth":4,
"field":"firstName",
"label":"First Name",
"type":"text"
},
{
"colWidth":4,
"field":"middleName",
"label":"Middle Name",
"type":"text"
},
{
"colWidth":4,
"field":"lastName",
"label":"Last Name",
"type":"text"
}
],
"contactDetails":[
{
"colWidth":6,
"field":"email",
"label":"Email",
"type":"email"
},
{
"colWidth":6,
"field":"phone",
"label":"Phone Number",
"type":"text"
}
],
"addressDetails":[
{
"colWidth":12,
"field":"address",
"label":"Address",
"type":"text",
"customClass":"mb-3"
}
]
}
📐 Handling Dynamic Layout Positioning
Different forms may require:
- multiple fields per row
- full-width sections
- responsive layouts
- custom spacing
Instead of hardcoding Bootstrap grid classes inside components, I added layout properties directly into the schema:
{
colWidth: 6,
field: 'email',
label: 'Email',
type: 'email',
}
This allowed the renderer to dynamically decide:
- how many fields appear per row
- how much width each field occupies
⚙️ Rendering the Form Dynamically
The renderer loops through grouped sections using Object.entries() and renders fields dynamically.
const DynamicFormRenderer = ({ schema }) => {
return (
<>
{Object.entries(schema)?.map(([sectionTitle, fields], index) => (
<div
key={index}
className='border rounded p-4 mb-4'>
<h3 className='mb-3 text-capitalize'> {sectionTitle} </h3>
<div className='row'>
{fields?.map((field, index) => {
const { colWidth, customClass } = field;
return (
<div
key={index}
className={`col-md-${colWidth} ${customClass || ''}`}>
{renderInputField(field)}
</div>
);
})}
</div>
</div>
))}
</>
);
};
🧩 Dynamic Field Rendering
Field rendering is handled through a centralized mapper function.
const renderInputField = (field) => {
const { type, label, options, field: fieldName } = field;
switch (type) {
case 'select':
return (
<CustomSelect
label={label}
options={options}
name={fieldName}
/>
);
default:
return (
<CustomInput
label={label}
name={fieldName}
type={type}
/>
);
}
};
This keeps rendering logic scalable as more field types are introduced later.
🏗️ Generating Initial Values Dynamically
Since fields are configuration-driven, initial values also need to be generated dynamically.
export const buildInitialValues = (schema) => {
const initialValues = {};
Object.values(schema).forEach((fields) => {
fields.forEach((field) => {
initialValues[field.field] = field.initialValue ?? '';
});
});
return initialValues;
};
This helps avoid manually maintaining large initial value objects.
✅ Benefits of This Approach
Reusability
The same renderer can support multiple forms.
Easier Maintenance
Adding fields becomes a config update instead of component rewrites.
Backend-Driven UI
Frontend becomes more flexible for enterprise workflows.
Scalable Architecture
This structure becomes extremely useful once:
- validations
- child fields
- API dropdowns
- multi-step flows
start getting introduced.
⚠️ Challenges That Appear Later
Dynamic rendering is actually the easy part.
The real complexity starts when handling:
- dynamic Yup schemas
- conditional child fields
- dependent dropdown APIs
- nested arrays
- performance optimization
- step-wise validation
That’s where architecture decisions start becoming important.
🚀 Next Part
In the next part of this series, I’ll cover:
👉 generating dynamic Yup validation schemas from JSON configuration using React + Formik.
That turned out to be much more interesting than the rendering itself.
Top comments (0)