By Sahil Prajapati.

Wizards are set of screens in an order that can be used to capture data, indicate progress of a particular process, or most importantly to help the user navigate a decision tree.

There is no single way in achieving wizard navigation nirvana (you heard this here first). Your customer experience could play out in a browser — desktop or mobile — or in an app. Setting up your mobile device the first time, makes use of a wizard (well the OS is the app here). Wizards, if used wisely, allay information panic.

You should have answers to handling data validation, handling the back button in the browser/device, steps where navigation does not make sense (you don’t need a back button on an acknowledgement screen, but what happens when the customer hits the back button in the browser). If you get your navigation wrong, your users will have a terrible time.

Our experience in using wizards have evolved over the years to reach a comfortable spot. We will take you through this evolution and discuss the merits and demerits with every such decision.

One step, One route

In the beginning, we decided to have every step within the wizard to be accessible through a route. As an example, we could take our user to a specific step in our sign-up flow as long as they had the right route.

Here is a code snippet using React Router as the routing library. This naive implementation does not have a logical wizard component. Instead every wizard step has a route that it mounts on top of.

This approach was simple although it turned out to be naive. Because of a missing orchestrator, we ended up with:

1. Components aware of navigation because they have to navigate to certain paths.

2. Wizard steps were exposed as routes. If an intermediate step was directly accessed, the wizard component broke.

3. No control over the browser back. On an acknowledgement screen that shows the Success message, we couldn’t control the browser back behaviour, unless we mutated the history stack.

The idea of components doing `history.push(…)` did not appeal to us as much. Also, our requirements pushed us to have an entire wizard flow under a single route. Having a centralized navigation under the wizard seemed to be the next logical step to take.

Render, render…

To control navigation under the wizard, the best approach seemed to capture the current step of the user in the wizard state. This removed the control from the browser history.

We decided to switch to wizards where all the screens are mapped to steps in a switch cases instead of routes (of-course there are multiple ways of writing a wizard).

Mapping screens to steps instead of route has its own advantage where screens `B` & `C` are not directly accessible to the user.

Now as the currentStep value changes (handled by our stores), user is taken through different screens without changing the route.

If you are careful enough, you might have noticed certain demerits from the route-based approach recurring here as well.

1. Every component needs to know the previous and next step. This gave us trouble when re-ordering screens, as the surface area of such changes were wide.

2. Navigating back in slightly complex flows, say Screen E can be reached from A or D, became tricky to deal with. This logic of how to go back now sat in components and often got duplicated across components.

Custom Navigation saves the day!

To address the concerns of losing navigation stack, we decided to maintain an internal stack in a separate Navigation library. Having this separate abstraction also helped us to toggle back behaviour in the wizard step to mimic the browser back when required.

This navigation layer needs to be initialised once per wizard. If a user hits browser back it just goes back to the previous route and exits the wizard.

This layer is agnostic of approaches like Flux/Redux. The only dependency would be the browser-history library for the toggle.

Here’s an overview of how the application works.

Feel free to take the demo app for a spin or tinker around with the code.

The outcome of this approach was quite satisfactory.

1. All wizard steps reside under a single route.

2. We have control over navigating forward and backward in a wizard.

3. When using a wizard in an Android mobile app, we could bind the device’s back button to the Wizard back button.

4. The call-sites for the Navigation library now sit neatly in the wizard. This brought down the surface area of change.

5. Since the library supports a custom navigation component (_navigation), it plays nicely with React Native.

Conclusion

Having a dedicated Navigation component eased our development process. The wizard steps could focus solely on the behaviour at that stage of the application and leave the routing concerns to the Navigation component. This also helped in seamlessly porting code to our React Native repo.