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.
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
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;
// 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;
// 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;
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;
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>
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>
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>
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>
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 theonClick
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>
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
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>
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>}
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>
);
};
Now let's create the config
object.
const config = {
navigation: {
component: Navigation,
location: "before", // or after
}
};
Finally, let's pass this object to our Steps
component.
<Steps config={config}>
// your Step components
</Steps>
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 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)
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.
location: "before" | "after" is basically telling the
Steps
component to render yourNavigation
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.
Finally, let's pass this object to our Steps component.
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!
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.
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?
This problem is solved with version @2.0.11
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.
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)
There might be two things that cause this:
Feel free to reach out if you encounter problems further.
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.
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 (
);
};
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
Very nice but is it working with checkbox and files?
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 toonChange
event of a checkbox field.props.getState('checkbox_name')
can be passed tochecked
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.@Samet Mutevelli please can you provide a example of using radio button or check button
Very nice lightweight package and docs :)
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.
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.Very nice package! May i know how can i add fadein fadeout transition for changing step?
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...
Is the value persisted if we press the previous button?
Yes, that's the whole point of holding values in a global state.
how to work with file upload ? its throwing error when i go previous stem
Shout out to Richard Mouser for emailing me pointing out a typo in one of the code snippets in the article!
Hey there,
it's possible, that I am just not seeing it. How can I get the saved values outside the Steps component to make an API request to save them in my database?
Thank you!