One way to deal with long, complex forms is to break them up into multiple steps. You know, answer one set of questions, move on to another, then maybe another, and so on and so forth. We often refer to these as multi-step forms (for obvious reasons), but others also take to calling it a “wizard” form.
Multi-step forms can be a great idea! By only showing a few inputs on a screen at a time, the form may feel more digestible and prevent users from feeling overwhelmed by a sea of form fields. Although I haven’t looked it up, I’m willing to say no one enjoys completing a ginormous form — that’s where multiple steps can come in handy.
The problem is that multi-step forms — while reducing perceived complexity on the front end — can feel complex and overwhelming to develop. But, I’m here to tell you that it’s not only achievable, but relatively straightforward using React as the base. So, that’s what we’re going to build together today!
Here’s the final product:
Let’s build it!
The easiest way to create a multi-step form is to create a container form element that contains all the steps inside of it as components. Here’s a visual showing that container (), the components inside of it (
,
,
) and the way states and props are passed between them.

serves as the container while three child components inside of it act as each step of the form.Although it seems to be more complex than a regular form, a multi-step form still uses the same principles as a React form:
- State is used for storing data and user inputs.
- Component is used for writing methods and the interface.
- Props are used for passing data and function into elements.
Instead of having one form component, we will have one parent component and three child components. In the diagram above, will send data and functions to the child components via props, and in turn, the child components will trigger a
handleChange()
function to set values in the state of . It’s one big happy family over here!We’ll need a function to move the form from one step to another as well, and we’ll get to that a little later.
The step child (get it?) components will receive props from the parent component for
value
and onChange
props.
component will render an email address input
will render a username input
will render a password input and a submit button
will supply both data and function into child components, and child components will pass user inputs back to the parent using its
props
.
Creating the step (child) components
First, we’ll create the form’s child components. We’re keeping things pretty barebones for this example by only using one input per step, but each step could really be as complex as we’d like. Since the child components look almost similar between one another, I’m just gonna show one of them here. But be sure to take a look at the demo for the full code.
class Step1 extends React.Component {render() {
if (this.props.currentStep !== 1) { // Prop: The current step
return null
}
// The markup for the Step 1 UI
return(
<div className="form-group">
<label htmlFor="email">Email address</label>
<input
className="form-control"
id="email"
name="email"
type="text"
placeholder="Enter email"
value={this.props.email} // Prop: The email input data
onChange={this.props.handleChange} // Prop: Puts data into state
/>
</div>
)}
}
Now we can put this child component into the form’s render()
function and pass in the necessary props. Just like in React’s form documentation, we can still use handleChange()
to put the user’s submitted data into state with setState()
. A handleSubmit()
function will run on form submit.
Next up, the parent component
Let’s make the parent component — which we’re all aware by now, we’re calling — and initialize its state and methods.
We’re using a currentStep
state that will be initialized with a default value of 1, indicating the first step () of the form. We’ll update the state as the form progresses to indicate the current step.
class MasterForm extends Component {
constructor(props) {
super(props)
// Set the initial input values
this.state = {
currentStep: 1, // Default is Step 1
email: '',
username: '',
password: '',
}
// Bind the submission to handleChange()
this.handleChange = this.handleChange.bind(this)
}
// Use the submitted data to set the state
handleChange(event) {
const {name, value} = event.target
this.setState({
[name]: value
})
}
// Trigger an alert on form submission
handleSubmit = (event) => {
event.preventDefault()
const { email, username, password } = this.state
alert(`Your registration detail: \n
Email: ${email} \n
Username: ${username} \n
Password: ${password}`)
}
// Render UI will go here...
}
OK, that’s the baseline functionality we’re looking for. Next, we want to create the shell UI for the actual form add call the child components in it, including the required state props that will be passed from via
handleChange()
.
render() {
return (
<React.Fragment>
<h1>A Wizard Form!</h1>
Step {this.state.currentStep}
<form onSubmit={this.handleSubmit}>
// Render the form steps and pass in the required props
<Step1
currentStep={this.state.currentStep}
handleChange={this.handleChange}
email={this.state.email}
/>
<Step2
currentStep={this.state.currentStep}
handleChange={this.handleChange}
username={this.state.username}
/>
<Step3
currentStep={this.state.currentStep}
handleChange={this.handleChange}
password={this.state.password}
/>
</form>
</React.Fragment>
)
}
One step at a time
So far, we’ve allowed users to fill the form fields, but we’ve provided no actual way to proceed to the next step or head back to the previous one. That calls for next and previous functions that check if the current step has a previous or next step; and if it does, push the currentStep
prop up or down accordingly.
class MasterForm extends Component {
constructor(props) {
super(props)
// Bind new functions for next and previous
this._next = this._next.bind(this)
this._prev = this._prev.bind(this)
}
// Test current step with ternary
// _next and _previous functions will be called on button click
_next() {
let currentStep = this.state.currentStep
// If the current step is 1 or 2, then add one on "next" button click
currentStep = currentStep >= 2? 3: currentStep + 1
this.setState({
currentStep: currentStep
})
}
_prev() {
let currentStep = this.state.currentStep
// If the current step is 2 or 3, then subtract one on "previous" button click
currentStep = currentStep <= 1? 1: currentStep - 1
this.setState({
currentStep: currentStep
})
}
}
We’ll use a get
function that will check whether the current step is 1 or 3. This is because we have three-step form. Of course, we can change these checks as more steps are added to the form. We also want to display the next and previous buttons only if there actually are next and previous steps to navigate to, respectively.
// The "next" and "previous" button functions
get previousButton(){
let currentStep = this.state.currentStep;
// If the current step is not 1, then render the "previous" button
if(currentStep !==1){
return (
<button
className="btn btn-secondary"
type="button"
onClick={this._prev}>
Previous
</button>
)
}
// ...else return nothing
return null;
}
get nextButton(){
let currentStep = this.state.currentStep;
// If the current step is not 3, then render the "next" button
if(currentStep <3){
return (
<button
className="btn btn-primary float-right"
type="button"
onClick={this._next}>
Next
</button>
)
}
// ...else render nothing
return null;
}
All that’s left is to render those buttons:
// Render "next" and "previous" buttons
render(){
return(
<form onSubmit={this.handleSubmit}>
{/*
... other codes
*/}
{this.previousButton}
{this.nextButton}
</form>
)
}
Congrats, you’re a form wizard! ?
That was the last step in this multi-step tutorial on multi-step forms. Whoa, how meta! While we didn’t go deep into styling, hopefully this gives you a solid overview of how to go about making complex forms less… complex!
Here’s that final demo again so you can see all the code in it’s full and glorious context:
React was made for this sort of thing considering it makes use of states, property changes, reusable components and such. I know that React may seem like a high barrier to entry for some folks, but I’ve written a book that makes it a much lower hurdle. I hope you check it out!
Recently, my team has been using React to create several similar Workflows.
We find it useful to pair the multi-step forms with React Context so that we don’t have to worry about passing in the common state data and methods into every child component as props.
https://reactjs.org/docs/context.html
Woah thanks for the info Russell! I have been hearing about Context API but haven’t got the time to explore it yet. Definitely will check that out now.
Great article, but wizard forms aren’t just about taking a step forward… I would like to hear how would you solve error handling, because that is the real challenge. Would you submit form on each step or on the last step? What if you submit form in the last step and server side validation respond with incorrect email address, which is on the first step, how to point user to that specific step to correct it?
Thx! :D
Actually, that’s a great idea for the next post! But the bird-eye view answer to your questions is that validation by the client side should be run on the previous and next button press. The bigger problem might be validating email. Is the email already registered? How do we check to the server to see if it’s dupe email? I will try to make a react wizard form deep dive where I will show it all with sample app next. Thx!
Although it is fun to use React to do this type of form. I would argue that it is needlessly too complex.
Here’s a simple solution using only HTML/CSS. You would need JS in order to disable the next button when the input is invalid:
Here’s some code that I was playing around with to show that your input is invalid. The submit button won’t submit if there are invalid inputs. But when you are stepping through each item and hiding the old ones this fails. So, you definitely want valid data through each step. And I’m sure the user would appreciate knowing that what they have entered is bad beforehand.
There’s a few concepts that are not mentioned here. I appreciate you can’t cover everything but I would interested to see how you would handle:
Each step being on it’s own route
Validation as a whole form, not just one step. Going back and changing values on previous steps might affect the validation of fields in later steps
Enabling/Disabling steps
There are a few more which I almost mentioned here, but realised it’s kind of outside the scope of this article.
Thanks for the read though. Strangely relevant to my current task.
Needs fewer classes, more Context, and hooks! Yeah React changed ones again!
Thanks for your article Nathan!
Just one tip to prevent logic leaking: The responsibility of showing/hiding steps is managed by the parent (in this case ), then it should be up to it to decide which child to show. In general, it’s more predictable to decide if a component should be rendered or not in the parent rather than in the children.
Having this logic in the parent would allow you to seamlessly reorder the steps if needed. They wouldn’t know which ‘step’ they are.
I’m very curious about this “get” function. I have not seen this key word before, and now I am wondering how this works. Google turns up nothing. I do agree with Julio with the rendering logic too.
While this form is great for simple 3 step form, there is a lot of similar code in each child component, and what if you want a 6 or 10 step form that would be more logic, could there be a way to extract from these components and make them more generic so they can be reused , wanted to know your thoughts???