# Building a Dynamic Form Renderer for Shopify

A Shopify form builder becomes much more useful when the same frontend code can render many different forms.

Instead of creating a separate React component for every form, you can store the form structure as data and let the renderer decide which fields to display.

This approach is useful for apps that allow store owners to create forms themselves.

In this article, we'll look at a practical way to build a dynamic form renderer for a Shopify app.

## What Is a Dynamic Form Renderer?

A dynamic form renderer takes a form configuration and converts it into UI.

For example, the backend may return:

```json
{
  "name": "Product Inquiry",
  "fields": [
    {
      "id": "name",
      "type": "text",
      "label": "Name",
      "required": true
    },
    {
      "id": "email",
      "type": "email",
      "label": "Email",
      "required": true
    },
    {
      "id": "question",
      "type": "textarea",
      "label": "Question",
      "required": true
    }
  ]
}
```

The renderer receives this object and generates the corresponding form.

The frontend doesn't need a hard-coded `ProductInquiry` component.

## Start With Field Components

The renderer needs components for the field types it supports.

For example:

```text
TextField
EmailField
TextareaField
SelectField
CheckboxField
RadioField
```

Each component should have a clear responsibility: display the field and handle its interaction.

## Map Field Types to Components

In React, a simple component map can make this easy:

```jsx
const fieldComponents = {
  text: TextField,
  email: EmailField,
  textarea: TextareaField,
  select: SelectField,
  checkbox: CheckboxField,
  radio: RadioField
};
```

Now the renderer can look up a component from the field type.

```jsx
const Component = fieldComponents[field.type];
```

This avoids a long chain of conditional statements.

## Build the Field Renderer

A reusable field renderer might look like:

```jsx
function FormField({ field, value, onChange, error }) {
  const Component = fieldComponents[field.type];

  if (!Component) {
    return null;
  }

  return (
    <Component
      field={field}
      value={value}
      onChange={onChange}
      error={error}
    />
  );
}
```

The field definition controls the configuration.

The React component controls the presentation.

## Render the Complete Form

The form renderer can then loop through the fields:

```jsx
function DynamicForm({ form }) {
  const [values, setValues] = useState({});

  const updateValue = (fieldId, value) => {
    setValues((current) => ({
      ...current,
      [fieldId]: value
    }));
  };

  return (
    <form>
      {form.fields.map((field) => (
        <FormField
          key={field.id}
          field={field}
          value={values[field.id]}
          onChange={(value) =>
            updateValue(field.id, value)
          }
        />
      ))}

      <button type="submit">
        Submit
      </button>
    </form>
  );
}
```

Now the same component can render many forms.

## Keep Field IDs Stable

Field IDs are important.

For example:

```json
{
  "id": "customer_email",
  "type": "email",
  "label": "Email"
}
```

The ID can be used for:

*   Form values
    
*   Validation
    
*   Error messages
    
*   Submission data
    
*   Editing
    
*   Analytics
    

The display label can change without breaking the data structure.

## Managing Form State

A dynamic form needs a consistent way to store values.

A simple structure is:

```javascript
const values = {
  customer_name: "Alex",
  customer_email: "alex@example.com",
  question: "Can I customize this product?"
};
```

When a field changes:

```javascript
setValues((current) => ({
  ...current,
  [field.id]: value
}));
```

This works regardless of which fields exist in the form.

## Handling Select Fields

Some fields need additional configuration.

For example:

```json
{
  "id": "reason",
  "type": "select",
  "label": "Reason",
  "options": [
    "Product Question",
    "Wholesale",
    "Custom Order"
  ]
}
```

The select component can render those options dynamically.

```jsx
<select value={value || ""} onChange={...}>
  {field.options.map((option) => (
    <option key={option} value={option}>
      {option}
    </option>
  ))}
</select>
```

The same renderer can support many different option lists.

## Handling Validation

The form schema can describe basic validation requirements.

For example:

```json
{
  "id": "email",
  "type": "email",
  "label": "Email",
  "required": true
}
```

The renderer can display a required indicator and client-side validation.

But the server must validate the submission independently.

A good flow is:

```text
React Form
    ↓
Client Validation
    ↓
API Request
    ↓
Server Validation
    ↓
Submission
```

Never treat frontend validation as a security boundary.

## Displaying Field Errors

A validation response can contain errors by field ID:

```json
{
  "success": false,
  "errors": {
    "email": "Please enter a valid email address.",
    "question": "This field is required."
  }
}
```

The renderer can pass the corresponding error to each field:

```jsx
<FormField
  field={field}
  value={values[field.id]}
  error={errors[field.id]}
/>
```

This keeps validation feedback close to the field that needs attention.

## Handling Unknown Field Types

What if the database contains a field type that the current frontend doesn't understand?

For example:

```text
type: "date"
```

but `date` hasn't been implemented yet.

Don't let the entire form crash.

A safe approach is:

```javascript
const Component = fieldComponents[field.type];

if (!Component) {
  return null;
}
```

In production, logging the unsupported type can also help developers detect configuration issues.

## Don't Put Targeting Logic in the Renderer

A Shopify app may need to decide which form should appear on the current page.

That is a separate responsibility.

For example:

```text
Shopify Context
       ↓
Form Targeting
       ↓
Matching Form
       ↓
Dynamic Renderer
```

The renderer should receive the selected form.

It shouldn't need to know whether the form came from:

```text
Homepage
Product page
Wholesale page
```

This separation keeps the code easier to maintain.

## Example Shopify Context

The targeting layer might produce:

```javascript
const context = {
  type: "product",
  handle: "custom-table"
};
```

It can then find the appropriate Active form.

After that:

```jsx
<DynamicForm form={matchingForm} />
```

The renderer only handles rendering.

## Keep Business Logic Outside Field Components

A field component should not be responsible for unrelated application logic.

For example, an email field shouldn't directly:

```text
Save submissions
Send emails
Find Shopify products
Determine form targeting
```

Instead:

```text
Field Component
→ Display input

Form Renderer
→ Build form

Validation
→ Validate values

Submission Service
→ Process data

Targeting
→ Find correct form
```

Each layer has one primary responsibility.

## Accessibility

Dynamic forms still need to follow basic accessibility practices.

Every input should have an understandable label.

For example:

```jsx
<label htmlFor={field.id}>
  {field.label}
</label>

<input id={field.id} />
```

Also consider:

*   Keyboard navigation
    
*   Visible focus states
    
*   Useful error messages
    
*   Correct input types
    
*   Accessible checkbox and radio controls
    
*   Sufficiently large mobile controls
    

A dynamic renderer should generate accessible markup by default.

## Mobile Considerations

Shopify stores receive visitors on phones and tablets.

The renderer should not assume that forms are only displayed on desktop.

Consider:

```text
Responsive widths
Readable labels
Large touch targets
Simple spacing
Mobile-friendly selects
```

The schema should describe the form.

The UI layer should determine how that form responds to different screen sizes.

## Performance Considerations

If a form contains many fields, avoid unnecessary re-renders where possible.

For larger forms, you can consider:

*   Memoizing field components
    
*   Splitting complex components
    
*   Avoiding unnecessary state updates
    
*   Lazy loading rarely used field types
    

For most small contact forms, a simple React implementation will be sufficient.

Don't optimize prematurely.

Measure first.

## Connecting the Renderer to a Node.js API

A typical architecture could be:

```text
Shopify Store
      ↓
Get Matching Form
      ↓
React Dynamic Renderer
      ↓
Customer Input
      ↓
POST /api/forms/:id/submit
      ↓
Node.js Validation
      ↓
Save Submission
```

The API should validate the form and submitted values before saving anything.

## Example Submission Request

The frontend might send:

```json
{
  "values": {
    "customer_name": "Alex",
    "customer_email": "alex@example.com",
    "question": "Can I order this in another size?"
  }
}
```

The backend already knows which form is being submitted from the route or form identifier.

It can load the stored schema and validate the values against it.

## How Cruxtab Contact Form Builder Uses Dynamic Forms

I built Cruxtab Contact Form Builder around the idea that Shopify store owners should be able to create forms without manually creating a new frontend component for every form.

Cruxtab Contact Form Builder supports forms that can be displayed on:

*   The Shopify homepage
    
*   Specific pages
    
*   Specific products
    

The general architecture is:

```text
Form Configuration
        ↓
Target Matching
        ↓
Dynamic Renderer
        ↓
Customer Input
        ↓
Validation
        ↓
Submission
```

This makes the storefront form system reusable.

## When a Dynamic Renderer Makes Sense

This approach is especially useful when:

*   Store owners create their own forms.
    
*   Forms change frequently.
    
*   Different products need different questions.
    
*   Different pages need different forms.
    
*   New field types may be added later.
    
*   You want one reusable frontend system.
    

For a single fixed contact form, a dynamic renderer may be unnecessary.

For a form builder, it can significantly reduce duplicated code.

## Try Cruxtab Contact Form Builder

If you want to create customizable forms for your Shopify homepage, specific pages, or specific products, check out Cruxtab Contact Form Builder:

[https://apps.shopify.com/cruxtab-form-builder](https://apps.shopify.com/cruxtab-form-builder)

## Final Thoughts

A dynamic form renderer separates the form definition from the UI implementation.

The basic pattern is:

```text
Schema
  ↓
Field Mapping
  ↓
React Renderer
  ↓
Validation
  ↓
Submission API
```

Keep targeting, rendering, validation, and submission as separate responsibilities.

This makes it easier to add new field types and support more Shopify use cases without rewriting the entire form system.

That is the approach behind the customizable form experience in Cruxtab Contact Form Builder.
