DEV Community

Cover image for Hands-On: Adaptive Forms with Conditional Rendering in React
Pixel Mosaic
Pixel Mosaic

Posted on

Hands-On: Adaptive Forms with Conditional Rendering in React

A compact, copy-paste-ready dev.to post that shows how to build adaptive forms in React using controlled components and conditional rendering. Drop the code into a create-react-app project's src/App.jsx (or paste directly into CodeSandbox) and you're ready to go.


Why adaptive forms?

Adaptive forms show or hide fields based on user choices. They're great for reducing friction (only ask what you need) and improving accessibility when implemented correctly.

This example covers:

  • Controlled inputs with useState
  • Conditional rendering (show/hide sections)
  • Simple validation on submit
  • Dynamic repeating fields (hobbies)
  • Accessible labels and brief UX polish

One-file copy-paste React code

Paste the entire block below into src/App.jsx of a Create React App, or into CodeSandbox. It uses only React (no external libs).

import React, { useState } from 'react';

export default function AdaptiveForm() {
  const [form, setForm] = useState({
    name: '',
    age: '',
    contactMethod: 'email', // 'email' | 'phone'
    email: '',
    phone: '',
    employed: 'no', // 'yes' | 'no'
    company: '',
    title: '',
    hobbies: [''],
  });

  const [errors, setErrors] = useState({});
  const [submittedData, setSubmittedData] = useState(null);

  function handleChange(e) {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  }

  function handleHobbyChange(index, value) {
    setForm(prev => {
      const hobbies = [...prev.hobbies];
      hobbies[index] = value;
      return { ...prev, hobbies };
    });
  }

  function addHobby() {
    setForm(prev => ({ ...prev, hobbies: [...prev.hobbies, ''] }));
  }

  function removeHobby(i) {
    setForm(prev => ({ ...prev, hobbies: prev.hobbies.filter((_, idx) => idx !== i) }));
  }

  function validate() {
    const err = {};
    if (!form.name.trim()) err.name = 'Name is required.';
    if (!form.age || Number(form.age) <= 0) err.age = 'Enter a valid age.';
    if (form.contactMethod === 'email') {
      if (!form.email.trim()) err.email = 'Email is required.';
      else if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email)) err.email = 'Invalid email.';
    } else {
      if (!form.phone.trim()) err.phone = 'Phone is required.';
      else if (!/^\+?\d{7,15}$/.test(form.phone)) err.phone = 'Enter phone with country code (digits only).';
    }
    if (form.employed === 'yes') {
      if (!form.company.trim()) err.company = 'Company name required.';
      if (!form.title.trim()) err.title = 'Job title required.';
    }
    return err;
  }

  function handleSubmit(e) {
    e.preventDefault();
    const v = validate();
    setErrors(v);
    if (Object.keys(v).length === 0) {
      // Clean empty hobby entries
      const cleaned = { ...form, hobbies: form.hobbies.filter(h => h && h.trim().length > 0) };
      setSubmittedData(cleaned);
    } else {
      setSubmittedData(null);
    }
  }

  return (
    <div style={{ maxWidth: 780, margin: '24px auto', fontFamily: 'system-ui, Arial', padding: 18 }}>
      <h1 style={{ fontSize: 26 }}>Adaptive form demo</h1>
      <p style={{ color: '#444' }}>This form adapts to user choices (contact method, employment) and supports dynamic fields.</p>

      <form onSubmit={handleSubmit} noValidate>
        <label style={{ display: 'block', marginTop: 12 }}>
          Name
          <input
            name="name"
            value={form.name}
            onChange={handleChange}
            style={{ display: 'block', width: '100%', padding: 8, marginTop: 6 }}
            aria-invalid={!!errors.name}
            aria-describedby={errors.name ? 'err-name' : undefined}
          />
        </label>
        {errors.name && <div id="err-name" style={{ color: 'crimson', fontSize: 13 }}>{errors.name}</div>}

        <label style={{ display: 'block', marginTop: 12 }}>
          Age
          <input
            name="age"
            type="number"
            min="0"
            value={form.age}
            onChange={handleChange}
            style={{ display: 'block', width: 120, padding: 8, marginTop: 6 }}
            aria-invalid={!!errors.age}
            aria-describedby={errors.age ? 'err-age' : undefined}
          />
        </label>
        {errors.age && <div id="err-age" style={{ color: 'crimson', fontSize: 13 }}>{errors.age}</div>}

        <fieldset style={{ border: 'none', padding: 0, marginTop: 12 }}>
          <legend style={{ fontWeight: 600 }}>Preferred contact method</legend>
          <label style={{ marginRight: 12 }}>
            <input
              type="radio"
              name="contactMethod"
              value="email"
              checked={form.contactMethod === 'email'}
              onChange={handleChange}
            />{' '}
            Email
          </label>
          <label>
            <input
              type="radio"
              name="contactMethod"
              value="phone"
              checked={form.contactMethod === 'phone'}
              onChange={handleChange}
            />{' '}
            Phone
          <
Enter fullscreen mode Exit fullscreen mode

Top comments (0)