DEV Community

Cover image for Building Dynamic Forms in React + Formik Using JSON Configuration (Part 1)
Shivani R
Shivani R

Posted on

Building Dynamic Forms in React + Formik Using JSON Configuration (Part 1)

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"
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

📐 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',
}
Enter fullscreen mode Exit fullscreen mode

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>
            ))}
        </>
    );
};


Enter fullscreen mode Exit fullscreen mode

🧩 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}
                />
            );
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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)