Becoming a Form Wizard: Intuitive Multi-Step Workflows

Rate this content
Bookmark

Forms are a core part of many applications and complex actions are often broken up into multiple forms as steps in this workflow. Using React’s Context API and a conventional state machine, we can build a reusable system for building these wizards and make the web a bit more magical.

26 min
21 Jun, 2022

Video Summary and Transcription

This Talk explores the concept of form wizards and their role in creating intuitive, multi-step workflows. It discusses the use of state machines and various implementation options, including Formic and the Dream API. The process of building a form wizard using React context, reducers, and custom hooks is explained. Integrating state machines and the introduction of the Robo Wizard library are also covered, highlighting its flexibility and compatibility with different UI frameworks.

Available in Español

1. Introduction to Form Wizard

Short description:

Welcome to Becoming a Form Wizard, a talk about building intuitive, multi-step workflows powered by state machines. My name is Nick Hare, a staff software engineer on the front end platform team at Betterment in New York City. Today, we'll explore what a wizard is, how they are created, the role of state machines, implementation options, and future directions.

Welcome to Becoming a Form Wizard, a talk about building intuitive, multi-step workflows powered by state machines.

My name is Nick Hare, I am a staff software engineer on the front end platform team at Betterment in New York City. In the past I've helped organize events and meetups such as Manhattan JS and now I organize an internal talk series at Betterment called Better Dev. On the side, I like to tinker with embedded JavaScript, I am also a fan of cycling outdoors and climbing indoors. You can find me online at hipsterbrown.com or at hipsterbrown on Twitter and GitHub.

On our journey today we're going to learn all about what is a wizard, how they are typically created, how state machines can help make them better, what we can use to implement them, and where we go from here.

2. Introduction to Wizards and User Flows

Short description:

Wizards are step-by-step guides that walk users through complex tasks. They break up the registration process into manageable steps, allowing users to focus on individual form sections. This pattern, known as a flow or workflow at Betterment, helps users avoid overwhelming and tedious experiences. It also ensures that users understand complex concepts and decisions. Betterment formalizes various tasks, such as creating goals and connecting external accounts, as flows, providing common conventions and utilities for their teams.

Wizards have been around for many years. While building applications, requesting user input to create records or perform actions is not always as simple as a form on a single page. Many user experiences require customers to click through multiple steps to submit all the information. This UI pattern is often called a wizard, a term dating back to the late 80s and early 90s, to indicate step-by-step guides that are designed to walk you through a complex task.

A popular source of this phraseology is the Microsoft Page Publisher wizard feature and the Connection wizard. However, the name usually reminds me of this wizard. Many services and apps will break up the registration process into a series of steps to make this feel easier, especially if there's quite a bit of information to gather in order to get started. Filling out long forms can be incredibly tedious, so wizards provide an experience where people can focus on the individual pieces of those forms with a sense of accomplishment with each step forward. Based on choices made through this process, the wizard can even help people skip irrelevant paths and potential confusion.

I think we've seen those sections on tax forms with a bunch of fine print conditionals that eventually lead to ignoring it altogether. This is what we want to avoid. This can be overwhelming, distracting, and even, as I said before, very tedious to go through. This is what we want to see instead. At Betterment, we call this pattern a flow or workflow and we use it quite a lot. As a financial services company, there's a lot of information and complex concepts to cover. We want to ensure our clients understand these decisions. Along with the usual sign-up tasks, we formalize creating goals, moving money, connecting external accounts, and more all as flows. Given how many we've had to build and maintain, it makes sense to have some common conventions and utilities for our teams to use.

3. Building Form Wizards with Formic

Short description:

So how are form wizards typically built? I searched for solutions when starting at Betterment and found examples using Formic, a form state manager for React. One downside is the lack of routing, causing the current step to be lost on page reload. Examples of routed multistep forms using number or name steps can be found in the same repository. However, these examples are tied to Formic and lack support for conditional paths. To address these concerns, I developed a new solution, the Dream API, which maintains the use of URLs, simplifies maintenance, and allows for gathering information without requiring it in every step.

So how are they typically built? If you've ever had to tackle this pattern in the past, you might have searched for multi-step forms or a form wizard for X, where X is the framework of choice for your product. I know I definitely looked around for such a solution when starting this journey at Betterment.

Naturally, I looked to the docs and examples for Formic, the form state manager of choice for React at Betterment. Looking at the Formic rep, we can find an example for a multi-step wizard that creates an abstract wizard component with wizard step child components. This allows for nice composition with splitting up a large form, and it could be reused across features as long as they use the same layout. One notable downside is the lack of routing. So the current step in the flow is lost if the page is reloaded.

And we can see our top level wizard component here, where we have an array of steps, and we have some next and previous actions in order to move through the wizard. We have our submission handler, which will allow this step to declare its own on submission, and we'll move forward to the next step unless we're on the last step, in which we perform a final submission. And it's all collected under a single format context with individual steps displayed and our back and submit buttons performing either the previous or next actions. And then as I said before, it will all be composed under the single wizard, wizard steps, and then each step declaring the fields in which the values that they care about are being gathered.

Now, in the same repo, we can find examples of routed multistep forms, one using number steps and the other using name steps. The numbered steps provide a simpler composition benefit as the first example, although numbers aren't too helpful as path names to communicate the intent of the step. With name steps, the ability to customize validation and form submission per step is lost. So we can see here are specific path locations for each page. We still have a wizard base and now a page component associated to pass through children instead of a step component. We still have our next and previous actions, some validation, handle submission, which progresses us forward next. And then we have some logic in order to display our high level formic context, our form, the active page, and then that previous and submission action again, all being composed under a single route with a wizard and their individual wizard pages, collecting all the fields in which they care about. And we can see our named pages here, which have individual page components, which now link to the next or even the previous step within the wizard, all collected as individual routes under the high-level router around a single form.

The greatest weakness with each of these examples is how tied they are to FormRec, which ultimately makes sense as these are examples for this library. However, many approaches found online will promote similar patterns, especially with unrouted steps. They are all generally focused on moving sequentially or in the order in which they're declared. It's not easy to see how conditional paths can be integrated. Looking at the purpose-built libraries, nothing appeared to solve all the concerns of building flows at Betterment, so that's when I set out to solve it myself. To help direct the work towards building this new solution, I came up with three primary directives. The first maintains the sense that good web experiences have URLs and aligned with the existing Rails-driven flows we built in the past. The second was learning from existing flows that made it complex to maintain them overall. The last one came from looking at our needs at Betterment, and how wizards may gather information but it's not always required per step. So this is the Dream API that we're going to implement. It looks a lot like we saw before with the Formic examples, where we have a high-level wizard and individual step components that will present a set of fields or whatever they wish to do, and these ones have names versus numbers or being unnamed at all.

4. Building the Form Wizard

Short description:

We'll use React context, a reducer hook, route components from React Router, and a custom hook to build our form wizard. The wizard reducer handles updating the current step and values. The step component maps the step name to the route path. The wizard context hook provides the wizard state and dispatch function. Custom functions simplify calling the dispatch actions. The API allows individual steps to compose the wizard. The handleSubmit function grabs form values and navigates to the next step.

And we're going to use this set of supplies in order to do it. We have our React context to use a reducer hook from React, route components from React Router, and a custom hook to pull it all together.

So we'll start with our React context to collect all the information we need to share between the high-level wizard and the individual steps. In our wizard component, we're going to iterate over the children in order to grab ones that match our step component that we'll build out, and look for the name prop and collect our list of names as our steps. We're going to set the default step as the first one in that list, and then declare our reducer.

The wizard reducer we'll look at next, but we're setting the initial state to the current step as the first step in the flow. The initial values to help seed any initial data that we're gathering in the flow, and that list of steps as well. And the value return by user reducer will be set as the wizard context provider. We're going to display all the children passed through our wizard component, and then initial redirect from the index of this feature to the default step. So anything directing or linking into this feature does not have to know what the first step is.

Now our wizard reducer is fairly simple as reducers go. We have two actions which contains the sync to update our current step, and then update values in which to, as the name suggests, update the values we're gathering throughout the lifetime of our wizard. And here's the step component we were referencing before. So we have our component prop and our name, and then we're going to pull in our use wizard context hook, we'll declare next in order to spread this state as props on our components. This is similar to other APIs that ReactRouter used to use in the past, and we're going to map the name of our step as the path of our route. And we'll see how useful having the state map to props on our component can be next.

In our wizard context hook, we're going to again, pull in that context using the use context hook from React. And as the result of useReducer, we're going to get the wizard state and that dispatch function in order to send actions to our reducer. We're going to wrap these dispatch actions into our own custom functions just to make it easier for anything consuming this hook to call them. So we have our sync action, our updateValues action, and then we have something called goToStep, which will use both of those in order to either call updateValues if we have them, and then call sync, and then push forward in history, or just push and navigate to the next step or previous step depending on how it's used. And so our next step will look for making sure that we're not on the last step in our in the wizard, and then call goToStep if we're not. versus the previous step, which will make sure we're not on the first step and allow us to call previous, if that's the case, and then we'll return it all as a result of our hook.

And so here's that API again, let's take a look at how an individual step can compose this. As we declared as part of one of our directives, that can be form agnostic. And so we have goToNextStep, goToPreviousStep, and wizard.state all coming from that hook the step component itself. But again, we could use that hook if we wanted to directly. This cleans up a little bit of boilerplate and having to always pull in that hook and allows for a bit of a dependency injection in case you want to test this individually later. And so our handleSubmit function just uses a regular form. And we're going to grab all the form values from the HTML form using the new form date API and this object from entries method, and then call goToNextStep with that object to values. We can even use existing values from wizard state in order to set the default on our input, and then very familiar looking actions, such as our previous button and submit in order to either go backwards or forwards.

5. Using Formic and Wizard State Values

Short description:

To clean up boilerplate, we can use formic. goToNextStep maps to onSubmit prop for formic. wizard state values are an object, so we can see initial values. Field inherits and binds to first name value. Actions remain the same with a button and submission handler. No form needed if only displaying wizard state.

If we want to clean up some of the boilerplate, we can use formic. And our goToNextStep maps exactly onto the onSubmit prop for formic, and our wizard state values is just an object, so we can use that to see the initial values for formic. And then we have our field, which will automatically inherit and be bound to that first name value within the wizard state values. And the actions can remain the same, because they are just a button and a submission handler. And no form is needed at all, because if we just want to display wizard state, then we can display it as we like.

6. Integrating State Machines

Short description:

The solution covers most of our usage and needs. State machines, also known as finite-state automata, provide a mathematical model of computation. A wizard is an interactive experience that displays one step at a time and can change based on external inputs. State diagrams help visualize the steps and transitions in a wizard. Guards allow for conditional navigation between steps. We can integrate a state machine within our wizard abstraction.

Now, the solution appears to cover most of our usage and needs. However, we still have a way of providing conditions without having to maybe update something specific within the individual step components, rather at this high level. So what happens when displaying a step or set of steps is dependent on a choice made in an earlier part of our wizard. And how do state machines help us solve this problem?

So for folks unfamiliar with state machines, they're formally known as finite-state automata. They provide a mathematical model of computation that describes the behavior of a system that can only be displayed one state at a given time. The ability to transition between these states is determined by specific events. Now if we wanted to relate that directly to a wizard, we can say a wizard is an interactive experience that can display exactly one of a finite number of steps at a given time. The wizard can change from one step to another in response to some external inputs or events. The change from one step to another is called navigation. A wizard is defined by a list of steps, its initial step and the conditions for each navigation. We can use this idea of modeling our wizards as state machines in the form of state diagrams.

So this state diagram is built using Text and MermaidJS in order to generate the visualization. And so we can see our individual states, or steps, start, about, and complete. Can perform transitions or navigations using next or previous in order to go forward or backwards. And this allows us to even use things like the stately AI visualizer to visualize this in a drag and drop or even code based manner. As I said before, we can control the conditions of each navigation. So looking at a modified version of that diagram we saw before, we can see that start receives a next event and then may progress to either the Gandalf or Merlin based on what is known as a guard. And so this guard looks at some logic to see if we have an interest in Gandalf in order to progress there, or otherwise falling back to Merlin. And each of those steps will progress next to complete. And here it is again using the stately AI visualizer to give us another viewpoint and we see that guard declared in that next to Gandalf. And this always makes me think of how Gandalf will help guard the path for the journey to wherever they're going. And so we can create this within our wizard abstraction using a function that takes in the current values of the wizard and any next state passed to go to next step. And looking at the values in the next state, if we have an interest in Gandalf, then it will return true. We can declare the list of available next steps on our start component, overriding the default behavior of going in sequence in which they're declared. And so in that array, we can also declare another array as the tuple of logic. And so we have our Gandalf, when, and we'll go there when we're interested in Gandalf. Otherwise, we'll fall back to the Merlin step. And on the individual Merlin and Gandalf steps, we can override that default behavior of next and previous by just declaring what we want to go to next without any sort of conditions. And you can see a similar pattern being used by Cassidy Williams in her talk, choosing your own adventures in Next.js, where she uses XState in order to do a similar journey through Next.js. So we went through building our Wizard using just React and a set of other utilities within there, but we didn't see how we can integrate a state machine within that pattern.

7. Introduction to Robo Wizard

Short description:

Robo Wizard is a library for building intuitive multi-step workflows backed by a state machine. It can be used with any UI and form manager, even plain HTML. It uses XState FSM to power the workflow and provides an intuitive API. There are three official packages for RoboWizard, including the core package, React integration, and React Router integration. Examples show how to integrate RoboWizard into various frameworks. The XState machine configuration allows you to create the machine for RoboWizard. A practical example of RoboWizard using React Router integration is demonstrated.

Well, in order to make all that easier, I've created a library called Robo Wizard. And Robo Wizard is all about building intuitive multi-step workflows backed by state machine. As we just saw here, it doesn't rely on anything to do with React or any specific framework or form. So you can bring your own UI and form manager even if it's just HTML. And we're using XState FSM, which is a subset of the wider XState package, in order to power all of this underneath a intuitive API.

And so this was originally inspired by a package called Robot, which allowed us to power all of this using functional composition. However, I eventually switched it over to using XState FSM in order to access the wider community of XState and the visualizer and all the APIs that it provides. RoboWizard, at its core, looks like this. We have a createWizard function, which takes in a list of steps, and then any initial values that we want to gather throughout the lifetime of that wizard. And when we start the wizard, we can list for any changes to the step and current values. We can call goToNextStep, goToPreviousStep, all familiar looking for what we saw before. And there are currently three official packages for RoboWizard, including this core package we see here, the RoboWizard React integration and the RoboWizard React Router integration for those routed navigable steps. However, there are a handful of example projects within the repo to show how RoboWizard can be integrated into Vue, Svelte, Alpine, and again, plain old HTML.

So why don't we take a look at what it looks like to create this machine using the XState machine configuration by yourself. If you wanted to mostly recreate RoboWizard, this is how you would do it. That one line in the preview example creates this complete JSON object. We have our JavaScript object, not just JSON. We have an initial step here. We have our context of the values we're gathering or individual states, what events will trigger going to the next state or step and the actions we take upon that event and transition. We can see here, you can go next to about, and we'll update values along that. Previous from about will go back to start, so on and so forth. Let's see a practical example of RoboWizard in the browser. This is RoboWizard React router integration using code sandbox demo. We're going to gather some signup values for email, password, first name, last name, phone number and term agreements. We're using chakra UI React to provide a little bit of nice UI through this demo. And we can see our step and wizard components being pulled in from our shared package. And we're going to set some initial values and declare our list of steps, all going in order and not overriding any of the initial behavior. If we look at an individual step such as our email, we can see we're pulling in the use wizard contact hook, very familiar to what we built before. And on that wizard, we can grab current values for initial values and our submission handler, we're going to call go to next step with a values field called values. And we see we're still using the regular field components and a submit button, anything that wants to go backwards.

8. Building the Robo Wizard

Short description:

We can set a backwards go back button. We can sign up and become a robo wizard, filling out values. If we don't agree to the user agreement, we can send them in a different direction. We can visualize wizard behavior using tools like mermaid jest diagrams or the stately AI visualizer. We can define entry and exit steps, create branching flows, and trigger server-driven transitions. Help is needed to build official integrations and explore different use cases for RoboWizard.

We can set a backwards go back button. And so looking at submitting this, let's create some initial values here. And so we've progressed forward on submission, I can go back using this back button, I can continue forward, I can also use the browser back buttons and forward buttons in order to keep track of our history.

And so within this wizard, we can sign up and become a robo wizard. So filling out a couple of values here, who knows why a wizard might need a phone number, we can see our big long terms, lorem ipsum in order to continue, we'll hit continue and progress forward. And we see here, we stopped, we're at the end, we're on success, we've now become a forum wizard, we can receive letters and alerts. And then learn more about this magic at robowizardjs.org.

Now, if we go back, we can see if we don't keep our values of agreeing to terms, we can still continue forward. Now, what if we wanted to send them in a completely different direction if they don't agree to the user agreement? Well, let's do that without touching any of the logic in the individual steps themselves. So I'm going to go over here and do some magic demo. I'm going to change some of the steps here to save us some time. And I'm going to pull on a pre created draft components and then also grab my guard function. And this guard function looks at are the next values being passed through and then sees looks for. If we agreed to the terms as being very explicit about it, we could just return the bully and that's existing there. And then we're going to pull in our when guard helper from Robo Wizard . And so let's go through this again without changing any of the existing steps, just staying at this high level. So doff dot gray. Pick something super secure. Just going to shorten this a little bit. And then if we don't agree to our lorem ipsum we continue forward and oh, no. Drat, the journey ends here. So shall we try again? Go back and continue. And now we've successfully submitted and become a forum wizard. And so where do we go from here? Well, a wizard's journey is never done. So how can we communicate better about wizards or communicate about wizards better while we can learn to visualize all this wizard behavior using tools like the mermaid jest diagrams or even the stately AI visualizer. And we can build upon that to create a shared language that all people that go into building these successful features can understand product design design and engineering. And we can add things like entry and exit steps, which allow us to define where we can go into the flow or the wizard and where, how we can exit. So things like redirects don't exist within individual steps themselves. But again, at that top level, we can define final steps, which allow us to pause or prevent any other navigation from that step, which means you can't go backwards again, or you can never go forwards in the case of success or where you don't want to resubmit some sort of action, or in the case of unrecoverable errors where they need to restart in order to try again. We can create branching flows or nested services in the case of something like XState, which allows us to pick a fork in the road and go to a completely different ending than when any other sort of condition would. And we can also trigger server driven transition. So before we saw how this integrated with the client side and client side navigation, what about something driven by Next.js or Remix or even some other general server backend. It doesn't even have to be done with JavaScript. However, XState makes this much easier to do altogether. And if all this sounds interesting to you, you can come help. We still need help with RoboWizard to build out, you know, official integrations for all those other frameworks people want to do, try all the different use cases for this library and learn all that we can do to build out a better experience this way. And so thank you and happy building. You can find out more at robowizard.js.org or at HapsAround on Twitter.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Vue.js London Live 2021Vue.js London Live 2021
34 min
Everything Beyond State Management in Stores with Pinia
Top Content
When we think about Vuex, Pinia, or stores in general we often think about state management and the Flux patterns but not only do stores not always follow the Flux pattern, there is so much more about stores that make them worth using! Plugins, Devtools, server-side rendering, TypeScript integrations... Let's dive into everything beyond state management with Pinia with practical examples about plugins and Devtools to get the most out of your stores.
React Advanced Conference 2022React Advanced Conference 2022
30 min
Using useEffect Effectively
Top Content
Can useEffect affect your codebase negatively? From fetching data to fighting with imperative APIs, side effects are one of the biggest sources of frustration in web app development. And let’s be honest, putting everything in useEffect hooks doesn’t help much. In this talk, we'll demystify the useEffect hook and get a better understanding of when (and when not) to use it, as well as discover how declarative effects can make effect management more maintainable in even the most complex React apps.
React Summit 2022React Summit 2022
20 min
Routing in React 18 and Beyond
Top Content
Concurrent React and Server Components are changing the way we think about routing, rendering, and fetching in web applications. Next.js recently shared part of its vision to help developers adopt these new React features and take advantage of the benefits they unlock.In this talk, we’ll explore the past, present and future of routing in front-end applications and discuss how new features in React and Next.js can help us architect more performant and feature-rich applications.
React Summit Remote Edition 2020React Summit Remote Edition 2020
30 min
React Query: It’s Time to Break up with your "Global State”!
Top Content
An increasing amount of data in our React applications is coming from remote and asynchronous sources and, even worse, continues to masquerade as "global state". In this talk, you'll get the lowdown on why most of your "global state" isn't really state at all and how React Query can help you fetch, cache and manage your asynchronous data with a fraction of the effort and code that you're used to.
React Day Berlin 2022React Day Berlin 2022
22 min
Jotai Atoms Are Just Functions
Top Content
Jotai is a state management library. We have been developing it primarily for React, but it's conceptually not tied to React. It this talk, we will see how Jotai atoms work and learn about the mental model we should have. Atoms are framework-agnostic abstraction to represent states, and they are basically just functions. Understanding the atom abstraction will help designing and implementing states in your applications with Jotai
JSNation 2022JSNation 2022
27 min
Announcing Starbeam: Universal Reactivity
Starbeam is a library for building reactive data systems that integrate natively with UI frameworks such as React, Vue, Svelte or Ember. In this talk, Yehuda will announce Starbeam. He will cover the motivation for the library, and then get into the details of how Starbeam reactivity works, and most importantly, how you can use it to build reactive libraries today that will work natively in any UI framework. If you're really adventurous, he will also talk about how you could use Starbeam in an existing app using your framework of choice and talk about the benefits of using Starbeam as the state management system in your application.

Workshops on related topic

React Summit 2020React Summit 2020
96 min
Rethinking Server State with React Query
Top Content
Featured Workshop
The distinction between server state and client state in our applications might be a new concept for some, but it is very important to understand when delivering a top-notch user experience. Server state comes with unique problems that often sneak into our applications surprise like:
- Sharing Data across apps- Caching & Persistence- Deduping Requests- Background Updates- Managing “Stale” Data- Pagination & Incremental fetching- Memory & Garbage Collection- Optimistic Updates
Traditional “Global State” managers pretend these challenges don’t exist and this ultimately results in developers building their own on-the-fly attempts to mitigate them.
In this workshop, we will build an application that exposes these issues, allows us to understand them better, and finally turn them from challenges into features using a library designed for managing server-state called React Query.
By the end of the workshop, you will have a better understanding of server state, client state, syncing asynchronous data (mouthful, I know), and React Query.
React Summit Remote Edition 2021React Summit Remote Edition 2021
71 min
State Management in React with Context and Hooks
WorkshopFree
A lot has changed in the world of state management in React the last few years. Where Redux used to be the main library for this, the introduction of the React Context and Hook APIs has shaken things up. No longer do you need external libraries to handle both component and global state in your applications. In this workshop you'll learn the different approaches to state management in the post-Redux era of React, all based on Hooks! And as a bonus, we'll explore two upcoming state management libraries in the React ecosystem.
React Advanced Conference 2022React Advanced Conference 2022
206 min
Best Practices and Patterns for Managing API Requests and States
Workshop
With the rise of frameworks, such as React, Vue or Angular, the way websites are built changed over the years. Modern applications can be very dynamic and perform multiple API requests to populate a website with fresh content or submit new data to a server. However, this paradigm shift introduced new problems developers need to deal with. When an API request is pending, succeeds, or fails, a user should be presented with meaningful feedback. Other problems can comprise API data caching or syncing the client state with the server. All of these problems require solutions that need to be coded, but these can quickly get out of hand and result in a codebase that is hard to extend and maintain. In this workshop, we will cover how to handle API requests, API states and request cancellation by implementing an API Layer and combining it with React-Query.
Prerequisites: To make the most out of this workshop, you should be familiar with React and Hooks, such as useState, useEffect, etc. If you would like to code along, make sure you have Git, a code editor, Node, and npm installed on your machine.