DEV Community

Cover image for How to create multi-step forms in React?
Samet Mutevelli
Samet Mutevelli

Posted on • Edited on

How to create multi-step forms in React?

GitHub logo sametweb / react-step-builder

React Step Builder allows you to create step-by-step interfaces easily.


This guide is using React Step Builder v2.0.11. If you are using the version v3+, please see the latest documentation on package's NPM page.


Creating a multi-step registration form was a challenge I faced a while back, which inspired me to create the react-step-builder package. In this post, I will make a quick demo on how to create a multi-step form using the package.

Let me briefly explain what the package does.

react-step-builder

It provides two wrapper components: Steps and Step.

Steps is a wrapper component for Step component(s), which takes your step components, combines their state in one location, and serves the helper methods for moving between them without losing the previously collected data.

Let's start with the demo which, I believe, will make it easier to understand the problem that the package is intended to solve.

For detailed explanations, please refer to the documentation.

1. Create a new project and install the package

$ npx create-react-app rsb-demo

$ npm install react-step-builder
Enter fullscreen mode Exit fullscreen mode

2. Have your step components ready

For the sake of simplicity, I will provide 3 sample components here. In the first and second components, we will ask our user to provide some info and, in the third step, render that info on the screen. Of course, in a real-life application, you probably will want to submit that data to some API of sorts. Also, you might have as many/big step components as you'd like.

At this point, step components will have zero functionality. We will empower them later with the provided methods without worrying about creating our form handlers and such.

// Step1.js
import React from "react";

function Step1(props) {
  return (
    <div>
      <p>Name: <input name="name" /></p>
      <p>Surname: <input name="surname" /></p>
    </div>
  );
}

export default Step1;
Enter fullscreen mode Exit fullscreen mode
// Step2.js
import React from "react";

function Step2(props) {
  return (
    <div>
      <p>Email: <input name="email" /></p>
      <p>Phone: <input name="Phone" /></p>
    </div>
  );
}

export default Step2;
Enter fullscreen mode Exit fullscreen mode
// FinalStep.js
import React from "react";

function FinalStep(props) {
  return (
    <div>
      <p>Name:</p>
      <p>Surname:</p> 
      <p>Email:</p>
      <p>Phone:</p>
    </div>
  );
}

export default FinalStep;
Enter fullscreen mode Exit fullscreen mode

3. Build your multi-step form

In your App.js file, import the wrapper components, and pass your newly created step components in.

// App.js
import React from "react";

import { Steps, Step } from "react-step-builder";
import Step1 from "./Step1";
import Step2 from "./Step2";
import FinalStep from "./FinalStep";

function App() {
  return (
    <div className="App">
      <Steps>
        <Step component={Step1} />
        <Step component={Step2} />
        <Step component={FinalStep} />
      </Steps>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

At this point, your step components will receive helper methods and properties in their props. We will utilize them to give our multi-step form some functionality.

4. Connect the form elements to the global state

Let's go back to our Step1 component and update our form elements and provide the state value for the value property and the handler method for the onChange event.

When you create an input like this: <input name="foo" />, the value for this element is saved in your global state with the foo key. So make sure you are giving unique names for each form element. That's what we will provide for the value property in our input elements.

Now let's access to our global state and update our input elements as such:

<input name="name" value={props.getState('name', '')} /></p>
<input name="surname" value={props.getState('surname', '')} /></p>
Enter fullscreen mode Exit fullscreen mode

If you realized, our getState method takes two parameters: The first one is the name of the input element, second is the default value. We pass an empty string that way we don't receive React's "uncontrolled/controlled component" warning in our console.

Now let's repeat the same changes in Step2 and FinalStep components as well.

// Step2.js
<input name="email" value={props.getState('email', '')} /></p>
<input name="phone" value={props.getState('phone', '')} /></p>
Enter fullscreen mode Exit fullscreen mode

There is no form element in the FinalStep component, we are just accessing the state data that has been entered by the user previously.

// FinalStep.js
<p>Name: {props.state.name}</p>
<p>Surname: {props.state.surname}</p>
<p>Email: {props.state.email}</p>
<p>Phone: {props.state.phone}</p>
Enter fullscreen mode Exit fullscreen mode

At this point, you might ask "why did we access the state with the props.getState('name', '') method earlier but with props.state.name in the last one. The answer is simple: this.props.name is undefined until your user starts typing in the field. However, props.getState('name', '') returns an empty string (thanks to the second parameter we passed) even if the user hasn't typed anything in the input yet. That way your form element gets its default value as an empty string so that you don't encounter the controlled/uncontrolled component error from React.

Now it is time to add onChange handlers so that our form saves user inputs into our global state.

Let's update our step components and give them a handler method for the onChange event.

<input name="name" value={props.getState('name', '')} onChange={props.handleChange} /></p>
<input name="surname" value={props.getState('surname', '')} onChange={props.handleChange} /></p>
Enter fullscreen mode Exit fullscreen mode

We did onChange={props.handleChange} to all of our form elements. It will make sure that our form values are saved with the correct key to our global state properly.

NOTE: You may also manipulate your global state with props.setState(key, value) method. It can be used for cases where synthetic React events (e.g. onChange) are not available. For example, clicking on an image or text and updating the state with the onClick method.

Our steps are ready now. Let's work on previous and next buttons so we can take a look around.

5. Utilize previous and next functionality

Every step will have props.next() and props.prev() methods for moving between steps. I will follow the first instinct and create Next and Previous buttons accepting those methods in their onClick events.

<button onClick={props.prev}>Previous</button>
<button onClick={props.next}>Next</button>
Enter fullscreen mode Exit fullscreen mode

You may add these buttons to every single step component individually or, to improve maintainability, you may also create a Navigation component. I will explain the Navigation component later in this post.

Now as the last step, let's talk about built-in methods of the individual steps.

6. Disable/conditionally render the navigation buttons

As it probably popped in your head, what if we don't want to show the Previous button in the first step component or the Next button in the last step component since there is no previous/next step in the first/last steps. The below-mentioned helper methods are very practical to solve this problem.

// From the documentation
props.step.isFirst() - Returns true if it's the first step, otherwise false
props.step.isLast() - Returns true if it's the last step, otherwise false
props.step.hasNext() - Returns true if there is a next step available, otherwise false
props.step.hasPrev() - Returns true if there is a previous step available, otherwise false
Enter fullscreen mode Exit fullscreen mode

If you would like to use the disable approach, you may do something like this:

<button disabled={props.step.isFirst()} onClick={props.prev}>Previous</button>
<button disabled={props.step.isLast()} onClick={props.next}>Next</button>
Enter fullscreen mode Exit fullscreen mode

And this is the conditional rendering approach:

{props.step.hasPrev() && <button onClick={props.prev}>Previous</button>}
{props.step.hasNext() && <button onClick={props.next}>Next</button>}
Enter fullscreen mode Exit fullscreen mode

Now let's add one global Navigation component to render in every step using the config object.

Create a Navigation component like this:

const Navigation = (props) => {
  return (
    <div>
    <button onClick={props.prev}>Previous</button>
    <button onClick={props.next}>Next</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now let's create the config object.

const config = {
  navigation: {
    component: Navigation,
    location: "before", // or after
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally, let's pass this object to our Steps component.

<Steps config={config}>
  // your Step components
</Steps>
Enter fullscreen mode Exit fullscreen mode

Update v.2.0.7
You can pass additional before or after properties to the config object. These properties accept a component identical to the Navigation component. As their name suggests, the component you pass to before / after property is rendered before/after the Step components.

NOTE: If you want to pass your own props to your step components, you may do so by simply passing props to Step components directly. Your step component will receive those props automatically.

Here is the final result:
react-step-builder demo

Here is a working example on codesandbox:

Please refer to the documentation as it provides a detailed explanation of each method and its use.

Top comments (25)

Collapse
 
xxcoledotxx profile image
xXCole-DOTXx

Step 6 is a bit confusing to me. So I make a component like formNav.tsx and enter the code you gave. That makes sense. But where does the config file go? How do I use it in App.tsx? Also, "location: "before", // or after" what do I do with this? How do you set it up to decide before or after? Everything before step 6 is phenomenally written but I really don't understand what's going on after that.

Collapse
 
sametweb profile image
Samet Mutevelli

location: "before" | "after" is basically telling the Steps component to render your Navigation component either before the form, or after.

And there is no config file, but there is a config object.

Perhaps you missed this part:

Now let's create the config object.

const config = {
  navigation: {
    component: Navigation,
    location: "before", // or after
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally, let's pass this object to our Steps component.

<Steps config={config}>
  // your Step components
</Steps>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
xxcoledotxx profile image
xXCole-DOTXx

Thanks for the reply! I guess I missed you refering to config as an object so that makes sense now. I was also really confused about where I was supposed to put the tags because you had put them in app.js. I now realize that you put them wherever you want the multi page form to start. Makes perfect sense. I love what you've done here and thanks again!

Collapse
 
devalo profile image
Stephan Bakkelund Valois

I'm getting a
SyntaxError: Cannot use import statement outside a module
module.exports = require("react-step-builder");

When trying to use it inside a React Next container.
Error happens as I'm importing the module into my component.

Collapse
 
sametweb profile image
Samet Mutevelli

Hello Stephan,
Although I am happy to help you with that, I am not very confident with my Next.js knowledge.
I researched the error message, and obviously, it helps in some cases if you add "type": "module" in your package.json file.
If that doesn't solve your issue, could you maybe reproduce the issue on codesandbox or share the repo with me so I can take a better look?

Collapse
 
sametweb profile image
Samet Mutevelli

This problem is solved with version @2.0.11

Collapse
 
krankj profile image
Sudarshan K J

I had the same problem. You can get around it by using Dynamic Imports in Next.js
Refer to the 3rd comment by 'acelaya' here: github.com/asyncapi/asyncapi-react... on how to get around this issue.

Collapse
 
louiemiranda profile image
Louie Miranda

When typing on an input field, why does it make the following error/warning?

1.chunk.js:55469 Warning: A component is changing an uncontrolled input of type undefined to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: fb.me/react-controlled-components
in input (at Step1.js:17)
in p (at Step1.js:17)
in div (at Step1.js:10)
in Step1 (created by Step)
in Step (at LabsBatch.js:21)
in Steps (at LabsBatch.js:20)
in LabsBatch (at App.js:646)
in Route (at App.js:642)
in Switch (at App.js:158)
in div (at App.js:155)
in App (at src/index.js:12)
in Router (created by BrowserRouter)
in BrowserRouter (at src/index.js:11)

Collapse
 
sametweb profile image
Samet Mutevelli • Edited

There might be two things that cause this:

  1. This tutorial is for version 2. So you probably installed the newer version but still following the old version's tutorial. In that case, simply provide your value for the input like this: value={props.getState('name', '')}
  2. You are using value={props.state.name} for the input element. You shouldn't prefer this usage for the form element's value prop. This usage is meant for only after you know for sure that that form element received a value already. 4. step in the tutorial explains this. Also, if you downloaded the latest version 2.0.4 (check your package.json), please refer to the recent documentation on the GitHub readme file. github.com/sametweb/react-step-bui...

Feel free to reach out if you encounter problems further.

Collapse
 
louiemiranda profile image
Louie Miranda

Thank you. Yes, I think I followed the old versions tutorial. When I added "value={props.getState('name', '')}" it worked with no errors now. Again, thank you and nice work here.

Collapse
 
jaishinawaraj profile image
Nawaraj Jaishi • Edited

When i try to add "after"/ "before" to the location it throw error to me saying tthis :
"(JSX attribute) config?: StepsConfig | undefined
Type '{ navigation: { component: (props: NavigationComponentProps) => JSX.Element; location: string; }; }' is not assignable to type 'StepsConfig'.
The types of 'navigation.location' are incompatible between these types.
Type 'string' is not assignable to type '"after" | "before" | undefined'.ts(2322)
index.d.ts(17, 5): The expected type comes from property 'config' which is declared here on type 'IntrinsicAttributes & StepsProps' "

here is my code why this type of error occure here?
my code snappit is following:

const AccountOpenForm = () => {

const config = {
navigation: {
component: StepNagivation,
location: "after",
},
};

return (








);
};

Collapse
 
kali70 profile image
Khaled Ben Amer • Edited

Hello Sir,
I found you package and I like it. However, when I tried to use it in my Web App I have some issues using it and I need to see if you can help me with that. My data structure which I need to use multi-step for to get data from the user as followinf

course {
name,
instructor,
descriptions: 'desc'
chapters: [
{
id: 0
name: 'name',
description: 'desc'
lessons: [
{
id: 0,
name: 'Lesson name',
description: 'desc'
videso: [
url1, url2, ..... url n
]
]
}
}
]
}

In step one, I need to add course info,
In step two, I need to fill chapter info, (Will have a select of multiple chapters and I can select one to update or add a new chapter
In step Three, I need to fill Lessons info, ( WIll have multiple lessons in a chapter and I need to select one to edit or create a new one.

The only thing I did using you package was add course and I have issues adding chapters and lessons since the state can not read the nested chapters and lessons inside the course object.

If you can help will be appreciated

Thank you
Khaled Ali

Collapse
 
mohsinalisoomro profile image
MOHSIN ALI SOOMRO

Very nice but is it working with checkbox and files?

Collapse
 
sametweb profile image
Samet Mutevelli

I just published an update. I missed the fact that checkboxes are a little different from regular text inputs/areas.

Now props.handleChange function can be passed to onChange event of a checkbox field. props.getState('checkbox_name') can be passed to checked property of the same field. About file inputs, I need to do more research. However, props.setState('key', 'value') method for updating state can be a workaround for now.

Collapse
 
jaishinawaraj profile image
Nawaraj Jaishi

@Samet Mutevelli please can you provide a example of using radio button or check button

Collapse
 
shunchiang profile image
Shun Chiang

Very nice lightweight package and docs :)

Collapse
 
isaacpro01 profile image
ssemugenyi isaac

Thanks @Samet, I would like to know if this package works with react native and if so, could you as well publish an article on it.

Collapse
 
sametweb profile image
Samet Mutevelli

Some parts of the package is designed to work with HTML elements. Since React Native is not using HTML elements (such as inputs, checkboxes, etc.) I would have to tweak the configuration a little bit to support React Native out-of-box.

However, I tried installing it and next/previous functions are working. If you can configure the onChangeText handler with the help of props.setState method, I don't see any reason it shouldn't work. Again, I didn't test it fully. I will come back here and let you know once I do that.

Collapse
 
nekogato profile image
nekogato

Very nice package! May i know how can i add fadein fadeout transition for changing step?

Collapse
 
sametweb profile image
Samet Mutevelli

The current API of the package does not have anything built-in for the transitions. However, you can use other React libraries to achieve that. Basically, add transitions
to your step components on the mount and unmount phase. You can check out this package: reactcommunity.org/react-transitio...

Collapse
 
novizh profile image
Naufal Ardhi

Is the value persisted if we press the previous button?

Collapse
 
sametweb profile image
Samet Mutevelli

Yes, that's the whole point of holding values in a global state.