You’ve all seen long forms… they are a nightmare even to look at! You probably have at least one in your app too. A checkout page, maybe.

Just today your designer said:

What if we split this checkout form into several “steps”? The users will feel a lot better about filling it!

You want your users to be awesome! So you took a stab and made this form multi-step. Every page is a form container of its own, and then there’s a “main” container which does the page switching and form submitting… Something like this, perhaps:

class CheckoutForm extends React . Component { constructor () { super (); this . state = { step : 1 }; this . goToNext = this . goToNext . bind ( this ); } goToNext () { const { step } = this . state ; if ( step !== 3 ) { this . setState ({ step : step + 1 }); } else { alert ( "Submitting" ); // BUT // how to access all the fields from here? } } render () { switch ( this . state . step ) { case 1 : return < Personal onSubmit= { this . goToNext } />; case 2 : return < Shipping onSubmit= { this . goToNext } />; case 3 : return < Billing onSubmit= { this . goToNext } />; } } } class Personal extends React . Component { constructor () { super (); this . state = { name : "" }; } } // and so on for Shipping and Billing

Here’s a JS Bin with a demo:

(The example is intentionally simplified to keep the sources brief. The real checkout form will likely be more complex: state and country selectors, credit card inputs, etc.)

Eh, cool, you think. But now… when the last step is submitted, how do you collect the input values from the previous steps?

Sending the data from the last step only is clearly not an option! The server will carelessly put a REJECTED 🚫 stamp onto your request if it gets only the billing info without the personal or shipping. It needs to know where to ship, after all…

Right now, each step “owns” its data — it’s simply not available outside of it. But we need it to be.

There are several directions we could take:

Ask each step for its data. Always render all the steps, and use CSS to hide the currently invisible steps. And then, use refs to ask each step about its values. While this may seem like a viable option at first, it will become unmanageable pretty quickly: certain fields often need to be editable from several steps; always keeping all steps rendered is pretty hacky; users want to go back to the previous step and expect their data to be there still; and so on.

Change the owner of the form state. So, instead of spreading the form state (aka field values) across three different components, it will live in one place — the main form component. The steps will receive the values as props, and call the callbacks when the fields change.

If it sounds anything like uncontrolled vs. controlled components… that’s because it is.

Currently, each step is “uncontrolled” from the perspective of CheckoutForm , which may seem simpler to implement, but it ends up being harder to maintain, read, and use — not so fit for any wizard form.

Instead, what we could do is make every step component receive the values for the fields as well as onChange handlers for each of these from the main form component. A step is just a bundle of controlled inputs, after all.

class CheckoutForm extends React . Component { constructor () { super (); this . state = { name : "" , shipping_line : "" , billing_line : "" }; // other inputs omitted for brevity // see full code on JS Bins } // goToNext and handleChange render () { switch ( this . state . step ) { case 1 : return ( < Personal name= { this . state . name } onChangeName= { this . handleChange ( "name" ) } onSubmit= { this . goToNext } /> ); case 2 : return ( < Shipping line= { this . state . shipping_line } onChangeLine= { this . handleChange ( "shipping_line" ) } onSubmit= { this . goToNext } /> ); case 3 : return ( < Billing line= { this . state . billing_line } onChangeLine= { this . handleChange ( "billing_line" ) } onSubmit= { this . goToNext } /> ); } } }

Then accessing all the field values will be pretty easy when handing onSubmit for Billing It’s all living in the state of CheckoutForm already and we can just use all the values we need to make a proper request:

class CheckoutForm extends React . Component { goToNext () { const { step } = this . state ; if ( step !== 3 ) { this . setState ({ step : step + 1 }); } else { const values = { name : this . state . name , shipping_line : this . state . shipping_line , billing_line : this . state . billing_line }; // submit to API somehow } } }

Here’s the final JS bin:

Sassy!

If you want to learn more about handling forms with React, be sure to sign up for my newsletter.