Skip to main content
New to FormEngine? It’s a React library that renders forms from JSON schemas — free, MIT-licensed. Start here →
TL;DR: Set renderWhen on any component in your FormEngine JSON schema. The expression runs on every state change — when it returns true the component renders; when it returns false it’s hidden and its validations are skipped automatically. Last reviewed: April 2026. This tutorial covers the common patterns for showing and hiding fields based on user input in FormEngine. The mechanism is renderWhen — an expression or function evaluated against the current form state. What you’ll learn: four patterns used in real forms — checkbox toggle, dropdown branch, cascading reveal, and full section toggle. Prerequisites: FormEngine Core installed. Familiarity with the basic FormViewer setup. FormEngine Core is open-source on GitHub. For visual conditional logic without writing JSON, see FormEngine Designer.

How renderWhen works

renderWhen sits alongside props and validations in a component’s schema entry. When present, FormEngine evaluates it on every state change and only renders the component when the result is true.
{
  "name": "companyName",
  "type": "Input",
  "props": { "label": "Company name" },
  "renderWhen": "form.data.accountType === 'business'"
}
Two important things to know:
  1. Hidden fields skip validation. If companyName above is required, that validation is ignored while the field is hidden. No need to write conditional validation logic — hiding is enough.
  2. Hidden fields keep their data. The value in form.data.companyName persists when the field is hidden. If you need to clear it, do so explicitly (see Clearing hidden values below).
The form object available in expressions:
PathTypeContents
form.dataRecord<string, unknown>Current field values
form.errorsRecord<string, unknown>Validation errors by field key
form.hasErrorsbooleanTrue if any validation error exists
form.stateRecord<string, unknown>Custom workflow state
form.parentDataRecord<string, unknown>Parent item data inside repeaters
form.indexnumber | undefinedCurrent repeater item index

Pattern 1: Checkbox toggle

Show an additional field when a checkbox is checked.
{
  "version": "1",
  "form": {
    "name": "ContactForm",
    "type": "form",
    "components": [
      {
        "name": "contactByPhone",
        "type": "Checkbox",
        "props": { "label": "Contact me by phone" }
      },
      {
        "name": "phoneNumber",
        "type": "Input",
        "props": {
          "label": "Phone number",
          "required": true
        },
        "renderWhen": "form.data.contactByPhone === true",
        "validations": [
          { "type": "required", "message": "Phone number is required" }
        ]
      }
    ]
  }
}
The required validation on phoneNumber only fires when the field is visible, so users who leave the checkbox unchecked never see a validation error for a field they can’t see.

Pattern 2: Dropdown branching

Show different fields based on which option the user selects.
{
  "version": "1",
  "form": {
    "name": "SupportForm",
    "type": "form",
    "components": [
      {
        "name": "issueType",
        "type": "Dropdown",
        "props": {
          "label": "What do you need help with?",
          "data": [
            { "label": "Billing", "value": "billing" },
            { "label": "Technical issue", "value": "technical" },
            { "label": "Account access", "value": "account" }
          ]
        }
      },
      {
        "name": "invoiceNumber",
        "type": "Input",
        "props": { "label": "Invoice number" },
        "renderWhen": "form.data.issueType === 'billing'",
        "validations": [
          { "type": "required", "message": "Invoice number is required for billing issues" }
        ]
      },
      {
        "name": "errorMessage",
        "type": "Textarea",
        "props": { "label": "Describe the error" },
        "renderWhen": "form.data.issueType === 'technical'",
        "validations": [
          { "type": "required", "message": "Please describe the technical issue" }
        ]
      },
      {
        "name": "accountEmail",
        "type": "Input",
        "props": { "label": "Email address on the account" },
        "renderWhen": "form.data.issueType === 'account'",
        "validations": [
          { "type": "required", "message": "Account email is required" },
          { "type": "email", "message": "Enter a valid email" }
        ]
      }
    ]
  }
}
Only one of the three conditional fields is visible at a time, and only that field’s validation runs.

Pattern 3: Cascading reveal

Show field B based on field A, and field C based on both A and B. Conditions compose naturally.
{
  "version": "1",
  "form": {
    "name": "CheckoutForm",
    "type": "form",
    "components": [
      {
        "name": "orderTotal",
        "type": "Input",
        "props": { "label": "Order total ($)" }
      },
      {
        "name": "hasPromoCode",
        "type": "Checkbox",
        "props": { "label": "I have a promo code" },
        "renderWhen": "Number(form.data.orderTotal) >= 50"
      },
      {
        "name": "promoCode",
        "type": "Input",
        "props": {
          "label": "Promo code",
          "required": true
        },
        "renderWhen": "Number(form.data.orderTotal) >= 50 && form.data.hasPromoCode === true",
        "validations": [
          { "type": "required", "message": "Enter your promo code" },
          { "type": "minLength", "value": 4, "message": "Promo codes are at least 4 characters" }
        ]
      }
    ]
  }
}
When the order total drops below 50, both hasPromoCode and promoCode hide — and the promo code value is preserved but ignored.

Pattern 4: Section toggle

Apply renderWhen to a container component to show or hide an entire section at once.
{
  "version": "1",
  "form": {
    "name": "OrderForm",
    "type": "form",
    "components": [
      {
        "name": "differentBilling",
        "type": "Checkbox",
        "props": { "label": "Use a different billing address" }
      },
      {
        "name": "billingSection",
        "type": "Container",
        "props": {},
        "renderWhen": "form.data.differentBilling === true",
        "components": [
          {
            "name": "billingName",
            "type": "Input",
            "props": { "label": "Billing name" },
            "validations": [
              { "type": "required", "message": "Billing name is required" }
            ]
          },
          {
            "name": "billingAddress",
            "type": "Input",
            "props": { "label": "Billing address" },
            "validations": [
              { "type": "required", "message": "Billing address is required" }
            ]
          },
          {
            "name": "billingZip",
            "type": "Input",
            "props": { "label": "ZIP code" },
            "validations": [
              { "type": "required", "message": "ZIP code is required" }
            ]
          }
        ]
      }
    ]
  }
}
All three fields inside billingSection — including their required validations — are inactive while the container is hidden.

4. Clearing hidden values on hide

By default, hidden field values persist in form.data. If you need to clear them when a field hides, use an action on the controlling field:
{
  "name": "accountType",
  "type": "Dropdown",
  "props": {
    "label": "Account type",
    "data": [
      { "label": "Personal", "value": "personal" },
      { "label": "Business", "value": "business" }
    ]
  },
  "actions": [
    {
      "event": "onChange",
      "action": "setValues",
      "args": {
        "companyName": null
      }
    }
  ]
}
This clears companyName whenever accountType changes. Only add this if stale data in hidden fields would cause problems — for most forms it doesn’t matter because hidden fields aren’t submitted.

Using function conditions for complex logic

When an expression becomes hard to read, switch to a function condition with computeType: "function":
{
  "name": "prioritySupportNote",
  "type": "Message",
  "props": {},
  "renderWhen": {
    "computeType": "function",
    "value": "const email = String(form.data.workEmail ?? ''); const opted = form.data.prioritySupport === true; return opted && !email.includes('@');"
  }
}
The function body must return true or false. It has access to the full form object.

Troubleshooting

Field shows when it shouldn’t — check the expression for type coercion issues. form.data.checkbox is true (boolean), not "true" (string). Test with === true not == 'true'. Validation fires on hidden field — this is a schema issue, not a bug. Check your renderWhen expression — if it’s returning a non-boolean truthy value instead of true, the field may be considered visible. Cascading condition doesn’t update — every renderWhen evaluates independently on state change. If field C depends on both A and B, write the full condition on C — don’t assume B’s visibility is enough. Cleared value immediately re-populates — if a computed property writes to the same field, the setValues action and the computed property race. Use form.state to gate the computed property instead.

Next steps

Last modified on April 16, 2026