Building a Multi-Step Registration Form with React

A simple React example of showcasing a multi-step registration where using state can determine what's rendered.

We've really enjoyed working with React here at Viget. We've used it on client projects, personal ventures, and most recently on Pointless Corp.

One great feature of React is how it handles the state of our application. Each time the state of a React component updates, React will rerender an updated view. Translation: React is great at showing the user something when something happens -- if it needs to.

I thought a good example of showcasing this ability would be in a multi-step registration where we update a component's state to show which step the user is on, then show the fields for that step accordingly. But before we dive in let's see what we'll be building.

Live Demo at CodePen and Github Repo

I added a little more markup, some CSS, and a progress bar to visualize the current step a little clearer. Other than that, we'll essentially be building the same thing.

Getting Started

We'll have a 4-step registration process. The user will:

  1. Enter basic account information
  2. Answer a short survey
  3. Confirm the information is correct
  4. Be shown a success message

An easy way to show just the relevant fields for a given step is to have that content organized into discrete components. Then, when the user goes to the next step in the process, we'll increase the step state by 1. React will see the change to our step state and automatically rerender the component to show exactly what we want the user to. Here's the basic code:

// file: Registration.jsx

var React         = require('react')
var AccountFields = require('./AccountFields')
var SurveyFields  = require('./SurveyFields')
var Confirmation  = require('./Confirmation')
var Success       = require('./Success')

var Registration = React.createClass({
	getInitialState: function() {
		return {
			step: 1
		}
	},

	render: function() {
		switch (this.state.step) {
			case 1:
				return <AccountFields />
			case 2:
				return <SurveyFields />
			case 3:
				return <Confirmation />
			case 4:
				return <Success />
		}
	}
}

module.exports = Registration

When the step is 1 (when our component is first loaded) we'll show the Account fields, at 2 we'll show Survey questions, then Confirmation at 3, and finally a success message on the 4th step. I'm including these components using the CommonJS pattern; each of these will be a React component.

Next, we'll create an object to hold the values our user will be entering. We'll have a nameemailpasswordage, and favorite colors fields. For now let's save this information as fieldValues at the top of our parent component (Register.jsx).

 // file: Registration.jsx

var fieldValues = {
  name     : null,
  email    : null,
  password : null,
  age      : null,
  colors   : []
}

// The rest of our file
...

Our first component we show to the user, <AccountFields />, contains the fields used to create a new account: namepassword, and email. When the user clicks "Save and Continue" we'll save the data and advance them to step 2 in the registration process.

// file: AccountFields.jsx

var React = require('react')

var AccountFields = React.createClass({
  render: function() {
    return ( <div>
      <label>Name</label> 
      <input type="text"
             ref="name"
             defaultValue={ this.props.fieldValues.name } />

      <label>Password</label>
      <input type="password"
             ref="password"
             defaultValue={ this.props.fieldValues.password } />

      <label>Email</label>
      <input type="email"
             ref="email"
             defaultValue={ this.props.fieldValues.email } />

      <button onClick={ this.saveAndContinue }>Save and Continue</button></div>
    )
  },

  saveAndContinue: function(e) {
    e.preventDefault()

    // Get values via this.refs
    var data = {
      name     : this.refs.name.getDOMNode().value,
      password : this.refs.password.getDOMNode().value,
      email    : this.refs.email.getDOMNode().value,
    }

    this.props.saveValues(data)
    this.props.nextStep()
  }
})

module.exports = AccountFields

Four things to note in <AccountFields />:

  1. defaultValue will set the starting value of our input. React does this instead of using the value attribute in order to account for some funkiness in how HTML handles default field input values. Click here to read more on this topic.
  2. We set the defaultValue to the associated this.props.fieldValues key, which is passed as properties from the parent component (Registration.jsx). This is so when the user saves and continues to the next step, but then goes back to a previous step, the input they've already entered will be visible.
  3. We are getting the value of these fields by referencing the DOM nodes using refs. To read up on how refs work in React check out this documentation. It's basically just an easier way of referencing a node.
  4. We'll need to create saveValues and nextStep methods in our Registration component (the parent), then pass it to <AccountFields /> (the child) as properties that they can reference. And since on step 2 of the process we'll have been able to go back to a previous step, we'll have to create a previousStep too.
// file: Registration.jsx
...

saveValues: function(fields) {
  return function() {
    // Remember, `fieldValues` is set at the top of this file, we are simply appending
    // to and overriding keys in `fieldValues` with the `fields` with Object.assign
    // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
    fieldValues = Object.assign({}, fieldValues, fields)
  }()
},

nextStep: function() {
  this.setState({
    step : this.state.step + 1
  })
},

// Same as nextStep, but decrementing
previousStep: function() {
  this.setState({
    step : this.state.step - 1
  })
},

...

From here we'll pass these newly created methods as properties to each of our child components so they can be called.

// file: Registration.jsx
...

render: function() {
  switch (this.state.step) {
    case 1:
      return <AccountFields fieldValues={fieldValues}
                            nextStep={this.nextStep}
                            saveValues={this.saveValues} />
    case 2:
      return <SurveyFields fieldValues={fieldValues}
                           nextStep={this.nextStep}
                           previousStep={this.previousStep}
                           saveValues={this.saveValues} />
    case 3:
      return <Confirmation fieldValues={fieldValues}
                           previousStep={this.previousStep}
                           submitRegistration={this.submitRegistration} />
    case 4:
      return <Success fieldValues={fieldValues} />
  }
}

You'll notice <AccountFields /> doesn't get passed previousStep since it's our first step and you can't go back. Also, instead of passing saveValues or nextStep to <Confirmation />, we pass a newly created submitRegistration method, which will handle submitting the users input (fieldValues) and increase the step of our registration process to 4, thus showing <Success />.

We would repeat the process of creating <AccountFields /> for the <SurveyFields /><Confirmation />, and <Success />  components. For the sake of brevity, you can check out the code on Github herehere, and here.

An Aside On Saving Data

Notice how we are saving user input and having to pass it (fieldValues={fieldValues}) to every component that needs it every time? Imagine we had even further nested components relying on this data, or were showing the data in multiple components that were being shown to the user at the same time, opening up the possibility of one having the most up-to-date data, but not the other? As you can see, our above implementation can quickly become tedious and brittle.

We can get ourselves out of this situation by saving our data in a storage entity that Facebook calls a Flux Store. From this Store we could save our data in a central location and rerender just the components that listened to changes to that data accordingly. If you're interested in learning a bit more on how this works I recommend checking out this talk by Pete Hunt.

Conclusion

React is awesome at handling what and when to show something to the user. In our example, what we're showing are related input fields (simple markup), and when those fields are shown is determined by the current step (the state of our Registration component). One way of thinking of this relationship is state determines shape. Depending on the state of our application, we're able to simply and predictably render something different, whether it's as small as a single character change or showing a completely different component altogether.

Have any questions about React or feedback on how I did something? Feel free to post a comment.

Tommy is a front-end developer who builds dynamic, interactive sites for Viget clients such as ESPN, Dick's Sporting Goods, and PUMA. He works in our Falls Church, VA, HQ.

More posts by Tommy