Back

Multi-step Form

Medium

Streak

0 days

Progress

0%

Submitted

0

Multi-step Form

React35 minMediumFree

Prompt

A Multi-Step Form (also called a wizard form) breaks a large form into smaller, logical sections spread across multiple steps. You see this pattern in onboarding flows, checkout processes, account setup wizards, and survey forms. The goal is to reduce cognitive load — instead of confronting the user with 15 fields at once, you present 3-5 fields per step. Your task is to build a multi-step form in React with at least three steps: Personal Info, Account Setup, and a Review & Submit step. The form should validate each step before allowing progression, maintain all entered data across steps (so going back doesn't clear fields), show a step progress indicator, and display a success state after final submission. This question tests your ability to architect shared state across multiple views, implement per-step validation logic, and build a clean step navigation system — all within a single component tree.

Requirements

  • →At least 3 steps: Personal Info (name, email), Account Setup (username, password), Review & Submit
  • →A visual step indicator showing progress (step dots or numbered steps)
  • →Next button validates the current step before advancing — shows inline errors if invalid
  • →Back button returns to the previous step without clearing data
  • →Review step shows all entered data for confirmation
  • →Submit button on final step shows a success state
  • →All form data persists across step navigation (going back and forth)
Example
Loading preview...
For the best coding experience, we recommend using a desktop device.
Preparing Sandbox...
Premium interview report

What interviewers score in this build

Use this before reading the code. It tells you what to say, what to test, and where machine-coding candidates usually lose points.

Interview signals

  • React: formData object holds all fields at the top level. errors object cleared per-field on change. currentStep drives rendering. validate(step, formData) is a pure function returning errors object.
  • HTML/CSS: Step indicator with numbered circles, connected by a line. Active step highlighted. Completed steps show a checkmark. form with onSubmit={e => e.preventDefault()} on each step.
  • Component Architecture: PersonalStep, AccountStep, ReviewStep are separate components receiving formData, onChange, errors as props. Validation logic in a pure validate(step, formData) function, not inside components.
  • State Management: formData: single flat object with all fields. errors: object cleared per-field on change, set on Next click. currentStep: 0-indexed integer. submitted: boolean for success state.

Time checkpoints

  1. 1

    0:00: Read prompt. Identify: shared formData at top level, per-step validation, currentStep navigation. Sketch the state shape.

  2. 2

    4:00: Set up formData state with all fields. Build step components as placeholders. Implement currentStep navigation (Next/Back).

  3. 3

    10:00: Build PersonalStep and AccountStep with controlled inputs wired to formData.

  4. 4

    15:00: Implement validate() pure function. Wire validation to Next button. Show inline errors.

Edge-case checklist

Empty data and first-load state
Slow network, failed request, and retry path
Keyboard navigation and focus movement
Large input size, re-render pressure, and cleanup

Common mistakes

  • Starting with JSX before naming state and events.
  • Ignoring accessibility until the final minute.
  • Over-building abstractions instead of finishing the required behavior.
  • Failing to narrate trade-offs while coding.
SolutionRead-only · Live Preview

Technical Explanation

Problem Understanding

The most important architectural decision in a multi-step form is: where does the form data live? The answer is always: at the top level, in the parent component. Not inside each step component. Here's why:

// ❌ Wrong — data split across step components
function Step1() { const [name, setName] = useState(''); ... }
function Step2() { const [email, setEmail] = useState(''); ... }
// Problem: when Step1 unmounts (user clicks Next), its state is gone.
// Going Back shows empty fields. Review step can't access Step1's data.
// ✅ Correct — all data at the top level
function MultiStepForm() {
  const [formData, setFormData] = useState({
    firstName: '', lastName: '', email: '',  // Step 1
    username: '', password: '',               // Step 2
  });
  // Step components receive formData as props — they never own state
}

With centralized formData, going back shows the values the user already entered. The review step can read all fields. Validation has access to all values at once. This is the only correct design.

💡 Interview Tip: Say this explicitly before coding: "I'm keeping all form data in a single object at the top level. Step components are purely controlled — they receive data as props and call an onChange handler. This means going back and forth never loses data, and the review step has access to everything."

State Shape

const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({
  firstName: '', lastName: '', email: '',    // step 0
  username: '', password: '',                // step 1
  // step 2 is review — no new fields
});
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);

The onChange Handler

function handleChange(field, value) {
  setFormData(prev => ({ ...prev, [field]: value })); // update one field
  if (errors[field]) {
    setErrors(prev => ({ ...prev, [field]: '' })); // clear error immediately
  }
}

Clearing the error for a field as soon as the user starts typing gives instant visual feedback — the red error message disappears the moment they address it. This is a much better UX than clearing all errors only on the Next click.

Validation as a Pure Function

function validate(step, data) {
  const errors = {};

  if (step === 0) {
    if (!data.firstName.trim()) errors.firstName = 'First name is required';
    if (!data.lastName.trim()) errors.lastName = 'Last name is required';
    if (!data.email.trim()) errors.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(data.email)) errors.email = 'Enter a valid email';
  }

  if (step === 1) {
    if (!data.username.trim()) errors.username = 'Username is required';
    if (data.password.length < 6) errors.password = 'Password must be at least 6 characters';
  }

  return errors; // empty object = valid
}

Extracting validation into a pure function (not inside a component) makes it independently testable and easy to extend. The function takes step and data, returns an errors object. An empty object means the step is valid. This pattern scales cleanly as you add more steps and more fields.

Next Button Logic

function handleNext() {
  const stepErrors = validate(currentStep, formData);

  if (Object.keys(stepErrors).length > 0) {
    setErrors(stepErrors); // show errors
    return;                // don't advance
  }

  setErrors({});           // clear all errors
  setCurrentStep(s => s + 1);
}

Step Rendering

const STEPS = ['Personal Info', 'Account Setup', 'Review'];

function renderStep() {
  switch (currentStep) {
    case 0: return <PersonalStep formData={formData} onChange={handleChange} errors={errors} />;
    case 1: return <AccountStep  formData={formData} onChange={handleChange} errors={errors} />;
    case 2: return <ReviewStep   formData={formData} />;
  }
}

No React Router needed. A simple switch on currentStep renders the right component. The step components are stateless — they just receive props and render fields.

Step Indicator

function StepIndicator({ current, total }) {
  return (
    <div className="step-indicator">
      {Array.from({ length: total }, (_, i) => (
        <React.Fragment key={i}>
          <div className={[
            'step-circle',
            i < current ? 'step-done' : '',
            i === current ? 'step-active' : '',
          ].join(' ')}>
            {i < current ? '✓' : i + 1}
          </div>
          {i < total - 1 && (
            <div className={`step-line ${i < current ? 'step-line-done' : ''}`} />
          )}
        </React.Fragment>
      ))}
    </div>
  );
}

Three visual states per step: upcoming (gray number), active (highlighted number), completed (checkmark + filled). The connecting line between steps also fills as steps are completed.

Review Step — All Data Visible

function ReviewStep({ formData }) {
  const fields = [
    ['First Name', formData.firstName],
    ['Last Name',  formData.lastName],
    ['Email',      formData.email],
    ['Username',   formData.username],
    ['Password',   '••••••••'], // never show actual password
  ];

  return (
    <div>
      <h3>Review Your Details</h3>
      {fields.map(([label, value]) => (
        <div key={label} className="review-row">
          <span className="review-label">{label}</span>
          <span className="review-value">{value || '—'}</span>
        </div>
      ))}
    </div>
  );
}
⚠️ Common Mistake: Displaying the actual password in the review step. Always mask it with '••••••••'. This is a security and UX expectation — the user has already set their password; they don't need to see it again.

Common Pitfalls Summary

  • State inside step components: Unmounting a step destroys its local state. Centralize everything at the parent level.
  • Not clearing errors on field change: Errors persist after the user has fixed the field — confusing and frustrating UX.
  • Resetting currentStep on browser refresh: Acceptable in an interview, but mention that production apps would persist step progress in URL params or sessionStorage.
  • Not masking the password on review: Security concern and bad UX.
  • Putting validation logic inside step components: Makes it impossible to run validation from the parent's Next click handler. Keep validation at the parent level or in a pure utility function.

How to Communicate During the Interview

  • "All form data lives in a single formData object at the top level. Step components are purely controlled — they never own state. This ensures going back never loses data."
  • "Validation is a pure function that takes the current step and formData and returns an errors object. An empty object means valid. I call it on every Next click."
  • "I clear a field's error the moment the user starts typing in that field — instant feedback is much better UX than waiting for the next Next click."
  • "On the review step, I mask the password — you never display plaintext passwords back to the user."

Interview Criteria

React

formData object holds all fields at the top level. errors object cleared per-field on change. currentStep drives rendering. validate(step, formData) is a pure function returning errors object.

HTML/CSS

Step indicator with numbered circles, connected by a line. Active step highlighted. Completed steps show a checkmark. form with onSubmit={e => e.preventDefault()} on each step.

Component Architecture

PersonalStep, AccountStep, ReviewStep are separate components receiving formData, onChange, errors as props. Validation logic in a pure validate(step, formData) function, not inside components.

State Management

formData: single flat object with all fields. errors: object cleared per-field on change, set on Next click. currentStep: 0-indexed integer. submitted: boolean for success state.

Edge Cases

Going back clears errors for that step. Resubmitting after success resets all state. Email validation with regex. Password minimum length. Review step handles empty optional fields gracefully.

Time Checkpoints

0:00

0:00: Read prompt. Identify: shared formData at top level, per-step validation, currentStep navigation. Sketch the state shape.

4:00

4:00: Set up formData state with all fields. Build step components as placeholders. Implement currentStep navigation (Next/Back).

10:00

10:00: Build PersonalStep and AccountStep with controlled inputs wired to formData.

15:00

15:00: Implement validate() pure function. Wire validation to Next button. Show inline errors.

20:00

20:00: Build ReviewStep showing all entered data. Build success state.

25:00

25:00: Add step indicator with completed/active/upcoming states. Polish layout.

Streak

0 days

Last active: Sign in to track

Progress

0%

0/0 solved

Submitted

0

Solutions pushed to review history.