Skip to Content
GuidesDynamic Forms

Dynamic Forms

Learn how to create forms that adapt based on user input and data.

💡 Tip

Dynamic forms make your UI more intuitive by showing only relevant fields based on user selections. This reduces cognitive load and improves user experience.

Learn how to create forms that adapt based on user input and data.

Conditional Fields

Show or hide fields based on form data using the ConditionalField component.

ℹ️ Info

Conditional fields are completely removed from the DOM when hidden, which improves performance and prevents validation issues.

Basic Conditional Field

import { ConditionalField, FormFieldHelpers } from "@rachelallyson/hero-hook-form"; const fields = [ FormFieldHelpers.checkbox("hasPhone", "I have a phone number"), ConditionalField({ name: "phone", label: "Phone Number", type: "input", condition: (values) => values.hasPhone === true, }), ];
💡 Tip

Always use explicit boolean comparisons (=== true) in conditions for clarity and to avoid truthy/falsy issues.

Complex Conditions

const fields = [ FormFieldHelpers.select("userType", "User Type", [ { label: "Individual", value: "individual" }, { label: "Business", value: "business" }, ]), ConditionalField({ name: "businessName", label: "Business Name", type: "input", condition: (values) => values.userType === "business", }), ConditionalField({ name: "taxId", label: "Tax ID", type: "input", condition: (values) => values.userType === "business" && values.businessName, }), ];

Multiple Conditions

const fields = [ FormFieldHelpers.checkbox("isVip", "VIP Customer"), FormFieldHelpers.checkbox("wantsNewsletter", "Subscribe to newsletter"), ConditionalField({ name: "vipCode", label: "VIP Code", type: "input", condition: (values) => values.isVip && values.wantsNewsletter, }), ];

Field Arrays

Create dynamic repeating field groups with the FieldArrayField component.

ℹ️ Info

Field arrays allow users to add and remove multiple instances of a field group, perfect for addresses, items, contacts, etc.

Basic Field Array

import { FieldArrayField, FormFieldHelpers } from "@rachelallyson/hero-hook-form"; const fields = [ FormFieldHelpers.input("name", "Name"), FieldArrayField({ name: "addresses", fields: [ FormFieldHelpers.input("street", "Street Address"), FormFieldHelpers.input("city", "City"), FormFieldHelpers.input("zipCode", "ZIP Code"), ], addButtonText: "Add Address", removeButtonText: "Remove Address", }), ];
💡 Tip

Set min and max props to control how many items users can add or remove.

Field Array with Validation

import { z } from "zod"; const schema = z.object({ name: z.string().min(2), addresses: z.array(z.object({ street: z.string().min(1, "Street is required"), city: z.string().min(1, "City is required"), zipCode: z.string().min(5, "ZIP code must be at least 5 characters"), })).min(1, "At least one address is required"), });

Field Array with Min/Max

FieldArrayField({ name: "skills", fields: [ FormFieldHelpers.input("name", "Skill Name"), FormFieldHelpers.select("level", "Level", [ { label: "Beginner", value: "beginner" }, { label: "Intermediate", value: "intermediate" }, { label: "Advanced", value: "advanced" }, ]), ], min: 1, max: 5, addButtonText: "Add Skill", removeButtonText: "Remove Skill", })

Field Array with Reordering

Enable users to reorder array items with up/down buttons:

FieldArrayField({ name: "slots", label: "Question Slots", fields: [ FormFieldHelpers.select("slotType", "Slot Type", [ { label: "Static", value: "STATIC" }, { label: "Dynamic", value: "DYNAMIC" }, ]), FormFieldHelpers.select("staticQuestionId", "Question", questions), ], enableReordering: true, reorderButtonText: { up: "↑", down: "↓", }, addButtonText: "Add Slot", removeButtonText: "Remove", })

Field Array with Custom Item Rendering

Customize the appearance of each array item:

FieldArrayField({ name: "items", label: "Items", fields: [ FormFieldHelpers.input("name", "Item Name"), FormFieldHelpers.input("quantity", "Quantity"), ], renderItem: ({ index, children, onMoveUp, onMoveDown, onRemove, canMoveUp, canMoveDown }) => ( <Card className="p-4 mb-4"> <div className="flex justify-between items-center mb-4"> <Chip>Item {index + 1}</Chip> <div className="flex gap-2"> <Button size="sm" onPress={onMoveUp} isDisabled={!canMoveUp}>↑</Button> <Button size="sm" onPress={onMoveDown} isDisabled={!canMoveDown}>↓</Button> <Button size="sm" color="danger" onPress={onRemove}>Remove</Button> </div> </div> {children} </Card> ), enableReordering: true, })

Field Array with Default Item

Control the default values when adding new items:

FieldArrayField({ name: "slots", fields: [ FormFieldHelpers.select("slotType", "Slot Type", options), FormFieldHelpers.select("staticQuestionId", "Question", questions), ], defaultItem: () => ({ order: 0, slotType: "STATIC", staticQuestionId: "", }), })

Conditional Fields Within Array Items

Fields within array items can be conditional based on other fields in the same item:

FieldArrayField({ name: "slots", fields: [ FormFieldHelpers.select("slotType", "Slot Type", [ { label: "Static", value: "STATIC" }, { label: "Dynamic", value: "DYNAMIC" }, ]), { ...FormFieldHelpers.select("staticQuestionId", "Question", questions), // This field only shows when slotType is "STATIC" dependsOn: "slotType", dependsOnValue: "STATIC", }, ], })
💡 Tip

The dependsOn path is automatically resolved relative to the array item. So "slotType" becomes "slots.0.slotType" for the first item.

Dynamic Sections

Group related conditional fields together with the DynamicSectionField component.

ℹ️ Info

Use dynamic sections when you need to show/hide multiple related fields together. They provide better visual organization than individual conditional fields.

Basic Dynamic Section

import { DynamicSectionField, FormFieldHelpers } from "@rachelallyson/hero-hook-form"; const fields = [ FormFieldHelpers.checkbox("hasEmergencyContact", "Has Emergency Contact"), DynamicSectionField({ name: "emergencyContact", title: "Emergency Contact Information", description: "Please provide emergency contact details", condition: (values) => values.hasEmergencyContact === true, fields: [ FormFieldHelpers.input("name", "Contact Name"), FormFieldHelpers.input("relationship", "Relationship"), FormFieldHelpers.input("phone", "Phone Number", "tel"), FormFieldHelpers.input("email", "Email", "email"), ], }), ];
💡 Tip

Dynamic sections can be nested for complex conditional logic. See the nested example below.

Nested Dynamic Sections

const fields = [ FormFieldHelpers.select("accountType", "Account Type", [ { label: "Personal", value: "personal" }, { label: "Business", value: "business" }, ]), DynamicSectionField({ name: "businessInfo", title: "Business Information", condition: (values) => values.accountType === "business", fields: [ FormFieldHelpers.input("businessName", "Business Name"), FormFieldHelpers.input("taxId", "Tax ID"), DynamicSectionField({ name: "billingAddress", title: "Billing Address", condition: (values) => values.businessName && values.taxId, fields: [ FormFieldHelpers.input("street", "Street Address"), FormFieldHelpers.input("city", "City"), FormFieldHelpers.input("state", "State"), FormFieldHelpers.input("zipCode", "ZIP Code"), ], }), ], }), ];

Advanced Patterns

Multi-Step Forms

const steps = [ { id: "personal", title: "Personal Information", fields: [ FormFieldHelpers.input("firstName", "First Name"), FormFieldHelpers.input("lastName", "Last Name"), FormFieldHelpers.input("email", "Email", "email"), ], }, { id: "address", title: "Address Information", fields: [ FormFieldHelpers.input("street", "Street Address"), FormFieldHelpers.input("city", "City"), FormFieldHelpers.input("zipCode", "ZIP Code"), ], }, { id: "preferences", title: "Preferences", fields: [ FormFieldHelpers.checkbox("newsletter", "Subscribe to newsletter"), FormFieldHelpers.checkbox("notifications", "Enable notifications"), ], }, ];

Dependent Dropdowns

const fields = [ FormFieldHelpers.select("country", "Country", [ { label: "Select Country", value: "" }, { label: "United States", value: "us" }, { label: "Canada", value: "ca" }, ]), ConditionalField({ name: "state", label: "State/Province", type: "select", condition: (values) => values.country === "us", selectProps: { options: [ { label: "Select State", value: "" }, { label: "California", value: "ca" }, { label: "New York", value: "ny" }, ], }, }), ConditionalField({ name: "province", label: "Province", type: "select", condition: (values) => values.country === "ca", selectProps: { options: [ { label: "Select Province", value: "" }, { label: "Ontario", value: "on" }, { label: "Quebec", value: "qc" }, ], }, }), ];

Dynamic Validation

import { z } from "zod"; const schema = z.object({ hasPhone: z.boolean(), phone: z.string().optional(), hasAddress: z.boolean(), address: z.object({ street: z.string(), city: z.string(), zipCode: z.string(), }).optional(), }).refine(data => { if (data.hasPhone && !data.phone) { return false; } if (data.hasAddress && (!data.address?.street || !data.address?.city)) { return false; } return true; }, { message: "Required fields are missing", });

Performance Considerations

Memoized Conditional Fields

import { memo } from "react"; const MemoizedConditionalField = memo(ConditionalField); // Use in your form const fields = [ MemoizedConditionalField({ name: "phone", label: "Phone", type: "input", condition: (values) => values.hasPhone, }), ];

Debounced Field Arrays

import { useDebouncedValidation } from "@rachelallyson/hero-hook-form"; function DynamicFieldArray({ fields, ...props }) { const { debouncedValue } = useDebouncedValidation(fields, { delay: 300 }); return ( <FieldArrayField {...props} fields={debouncedValue} /> ); }

Testing Dynamic Forms

Testing Conditional Fields

import { createFormTestUtils } from "@rachelallyson/hero-hook-form"; const testUtils = createFormTestUtils(form); // Test conditional field visibility testUtils.setFieldValue("hasPhone", true); cy.get('[data-testid="phone-field"]').should("be.visible"); testUtils.setFieldValue("hasPhone", false); cy.get('[data-testid="phone-field"]').should("not.exist");

Testing Field Arrays

// Test adding items cy.get('[data-testid="add-address"]').click(); cy.get('[data-testid="addresses[0].street"]').type("123 Main St"); // Test removing items cy.get('[data-testid="remove-address-0"]').click(); cy.get('[data-testid="addresses[0].street"]').should("not.exist");

Best Practices

1. Keep Conditions Simple

💡 Tip

Simple conditions are easier to understand, test, and maintain.

// ✅ Good: Simple condition condition: (values) => values.hasPhone === true // ❌ Avoid: Complex conditions condition: (values) => values.hasPhone && values.userType === "premium" && values.isActive && values.subscriptionStatus === "active"

2. Use Meaningful Field Names

// ✅ Good: Clear naming name: "emergencyContactPhone" // ❌ Avoid: Unclear naming name: "phone2"

3. Provide Clear Labels

// ✅ Good: Descriptive labels label: "Emergency Contact Phone Number" // ❌ Avoid: Generic labels label: "Phone"

4. Handle Edge Cases

⚠️ Warning

Always provide fallback values and handle undefined/null cases in conditions.

// ✅ Good: Safe condition with fallbacks const condition = (values) => { return values?.hasPhone === true && values?.phone?.length > 0; }; // ❌ Bad: No null checking const condition = (values) => { return values.hasPhone && values.phone.length > 0; // May throw error };

5. Test All Conditions

ℹ️ Info

Test both visible and hidden states to ensure conditional fields work correctly.

// Test both true and false states it("shows phone field when hasPhone is true", () => { // Test visible state }); it("hides phone field when hasPhone is false", () => { // Test hidden state });

Advanced Field Array Patterns

Using createFieldArrayCustomConfig Helper

For complex array UIs that need full control, use the createFieldArrayCustomConfig helper:

import { createFieldArrayCustomConfig, FormFieldHelpers } from "@rachelallyson/hero-hook-form"; const slotsConfig = createFieldArrayCustomConfig("slots", { label: "Question Slots", enableReordering: true, renderItem: ({ index, field, form, control, onMoveUp, onMoveDown, onRemove }) => ( <div className="border rounded-lg p-4"> <div className="flex justify-between"> <span>Slot {index + 1}</span> <div className="flex gap-2"> <Button onPress={onMoveUp}>↑</Button> <Button onPress={onMoveDown}>↓</Button> <Button onPress={onRemove}>Remove</Button> </div> </div> <SelectField name={`slots.${index}.slotType`} control={control} // ... other props /> </div> ), defaultItem: () => ({ order: 0, slotType: "STATIC", }), });

Syncing Arrays in Edit Forms

When editing existing data, use syncArrays to determine what to delete, update, and create:

import { syncArrays } from "@rachelallyson/hero-hook-form"; function EditTemplateForm({ template }) { const handleSubmit = async (data) => { const { toDelete, toUpdate, toCreate } = syncArrays({ existing: template.slots, current: data.slots, getId: (slot) => slot.id, }); // Delete removed slots await Promise.all(toDelete.map(slot => deleteSlot(slot.id))); // Update existing slots await Promise.all( toUpdate.map(({ existing, current }) => updateSlot(existing.id, current) ) ); // Create new slots await Promise.all(toCreate.map(slot => createSlot(slot))); }; }

Simple Forms

For single-field forms, use the SimpleForm component:

import { SimpleForm, FormFieldHelpers } from "@rachelallyson/hero-hook-form"; import { z } from "zod"; const messageSchema = z.object({ message: z.string().min(1, "Message cannot be empty"), }); function MessageInput() { return ( <SimpleForm schema={messageSchema} field={FormFieldHelpers.input("message", "", { placeholder: "Add a note...", endContent: ( <Button type="submit" isIconOnly> <SendIcon /> </Button> ), })} onSubmit={async (data) => { await sendMessage(data.message); }} hideSubmitButton /> ); }

Next Steps

Last updated on