Constructing a Multi-Step Form in React with Hooks

For all of you Bootstrap, developers who need to construct a multi-step form in React, here is this simple one.

It was built with Create React App and uses React Hooks and Bootstrap 4.

It displays a linear progress bar at the top. The user fills the details in stages, followed by GDPR information and a Thank You stage.

It is a single component where each stage is rendered according to the stage counter,

{
    stage === 4 && ...
}

App.js

import React, { useState } from 'react';
import './App.css';

const stages = ['name', 'contact', 'gdpr', 'thanks'];

function App() {
  const [stage, setStage] = useState(0);

  const [details, setDetails] = React.useState({
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    acceptGDPR: false
  });

  const processField = (name, value) => {
    setDetails({ ...details, [name]: value });
  }

  return (
    <div className="container">
      <h1>Register</h1>
      <div className="progress mt-3 mb-3">
        <div className="progress-bar" role="progressbar" style={{ width: `${ (stage + 1) / stages.length * 100 }%` }} aria-valuenow={ (stage + 1) / stages.length * 100 } aria-valuemin="0" aria-valuemax="100"></div>
      </div>
      {
        stage === 0 && 
        <form>
          <div className="form-group">
            <label htmlFor="firstName">First Name</label>
            <input 
              type="text" 
              className="form-control" 
              id="firstName" 
              aria-describedby="firstNameHelp" 
              value={details.firstName}
              onChange={e => processField('firstName', e.target.value)}
            />
            <small id="firstNameHelp" className="form-text text-muted">Required</small>
          </div>
          <div className="form-group">
            <label htmlFor="lastName">Last Name</label>
            <input 
              type="text" 
              className="form-control" 
              id="lastName" 
              aria-describedby="lastNameHelp"
              value={details.lastName}
              onChange={e => processField('lastName', e.target.value)}
            />
            <small id="lastNameHelp" className="form-text text-muted">Required</small>
          </div>
          <button 
            type="button" 
            className="btn btn-primary" 
            disabled={ !details.firstName && !details.lastName }
            onClick={ () => setStage(stage + 1) }
            style={{ opacity: !details.firstName && !details.lastName ? 0.2 : 1 }}
          >
            Next
          </button>
        </form>
      }
      {
        stage === 1 && 
        <form>
          <div className="form-group">
            <label htmlFor="email">Email</label>
            <input 
              type="text" 
              className="form-control" 
              id="email" 
              aria-describedby="emailHelp" 
              value={details.email} 
              onChange={e => processField('email', e.target.value)}
            />
            <small id="emailHelp" className="form-text text-muted">Required</small>
          </div>
          <div className="form-group">
            <label htmlFor="phone">Phone</label>
            <input 
              type="text" 
              className="form-control" 
              id="phone" 
              aria-describedby="phoneHelp"
              value={details.phone} 
              onChange={e => processField('phone', e.target.value)}
            />
            <small id="phoneHelp" className="form-text text-muted">Optional</small>
          </div>
          <button 
            type="button" 
            className="btn btn-primary" 
            disabled={ !details.email }
            onClick={ () => setStage(stage + 1) }
            style={{ opacity: !details.email ? 0.2 : 1 }}
          >
            Next
          </button>
        </form>
      }
      {
        stage === 2 &&
        <form>
          <blockquote className="blockquote mt-3 mb-3">
            <p className="mb-0">GDPR protects your data for sure ...</p>
          </blockquote>
          <div className="form-check mt-3 mb-3">
            <input 
              className="form-check-input" 
              type="checkbox" 
              value={ details.acceptGDPR } 
              id="acceptGDPR"
              onChange={e => setDetails({ ...details, acceptGDPR: e.target.value })}
            />
            <label className="form-check-label" htmlFor="acceptGDPR">
              Accept
            </label>
          </div>
          <button 
            type="button" 
            className="btn btn-primary" 
            disabled={ !details.acceptGDPR }
            onClick={ () => setStage(stage + 1) }
            style={{ opacity: !details.acceptGDPR ? 0.2 : 1 }}
          >
            Next
          </button> 
        </form>
      }   
      {
        stage === 3 && 
        <div>
          <div className="alert alert-success mt-3 mb-3" role="alert">
            Thank You - { details.firstName }
          </div>

          <h2>Your Recorded Details</h2>

          <dl className="row">
            <dt className="col-sm-3">First Name</dt>
            <dd className="col-sm-9">{ details.firstName }</dd>

            <dt className="col-sm-3">Last Name</dt>
            <dd className="col-sm-9">{ details.lastName }</dd>

            <dt className="col-sm-3">Email</dt>
            <dd className="col-sm-9">{ details.email }</dd>

            <dt className="col-sm-3">Phone</dt>
            <dd className="col-sm-9">{ details.phone }</dd>

            <dt className="col-sm-3">GDPR</dt>
            <dd className="col-sm-9">Accepted</dd>
          </dl>
        </div>
      }

    </div>
  );
}

export default App;

index.js

import 'bootstrap/dist/css/bootstrap.css';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Screenshots