At Makers and Markers, we’re expanding our platform from supporting artists selling their crafts to facilitating full-scale construction projects. This will significantly change our bottom line, but will also require some very long and complex forms.
Previously, our longest forms were our checkout forms, which were only a few pages long.
Managing these forms wasn’t particularly challenging. One parent component can keep the current page state, and each page can update that state as the user advances.
For some forms, we might have an array of page ids, so the components only need forward
 and back
functions, and the main component can manage what comes next. This may even come with a goTo
method for skipping around.
However, with the new specifications, it’s clear that these simple strategies won’t scale to meet the complexity of the upcoming forms.
But how do we manage these intricate flows without overwhelming both the developers and the users? Looking on the internet, the best advice is to use “multiple dependent forms.” This means that there are components for each page, each capable of saving its own data. It’s not entirely clear how this differs from the initial setup, and the advice doesn’t make it clear how to proceed with the more complex aspects of the requirements.
This document will cover the problem of managing the sequence and screens in long forms. This is a separate problem space than state management, for which I recommend React Hook Form. Another related problem is schema validation, which has many great solutions, including Zod and Yup.
Approaching the Problem
What key factors should we consider when designing a user-friendly and maintainable form flow?
There are a few common aspects to most flows that can guide us through this challenge:
- There is often a parent/child relationship with a category (checkout) and a page (shipping info).
- Many pages, and sometimes entire sections, are conditional.
- A flow can be represented as a graph.
- Although branching exists, the graph is mostly linear.
- Sometimes the order or sequence is irrelevant—in the example above, the plumbing and electrical sections can be completed in any order.
- Using unit tests to ensure that a flow goes through the right pages is very helpful.
The simple setup above, with a single parent component, does handle the form as a graph or sequence, but it does so with navigation logic embedded within the UI components. While this works for simpler forms, it can become difficult to maintain as complexity increases.
Representing the Flow
How can we separate the flow from the UI, and which approach suits our needs better—a graph that mirrors complex decision paths, or a sequence that simplifies the journey?
There are two major ways of implementing a flow: as a graph and as a series of conditional pages. Both have advantages, and neither is right for every situation. To manage complex flows effectively, we need to represent the navigation logic separately from the UI components. We'll use React Multi Page Form to separate the UI from the flow logic.
The Flow as a Graph
Because the specifications for a large form usually come as a flowchart, this is a natural way to structure your flow. The initial example, with each page setting the state of its parent component directly, embodies the spirit of the graph solution.
The strength of the graph solution is shown in the plumbing requirements screen.
// The next page for plumbing requirements is an easy split.
// It's one of two options, and we know what they are.
const plumbingRequirements = {
id: 'plumbing-requirements',
selectNextPage: (data) =>
greatLakeStates.has(data.state)
? 'great-lakes'
: 'plumbing-contractor',
isComplete: (data) => !!data.plumbingRequirements,
Component: PlumbingRequirements,
}
This screen is concerned only with plumbing data, directing the user to the right page is well within its scope.
However, let's look at the remaining two pages:
const greatLakes = {
id: 'great-lakes',
selectNextPage: () => 'plumbing-contractor',
isComplete: (data) => !!data.greatLakes,
Component: GreatLakes,
}
// For the final item in this flow, this page has the
// responsibility of returning the user to the right place
// in the entire form, which may be an unreasonable request
const selectPlumbingContractor = {
id: 'plumbing-contractor',
selectNextPage: (data) => ???,
isComplete: (data) => !!data.plumbingContractor,
}
The great lakes form is fine. It has one page before it and one page after it, both are within the scope of plumbing.
The plumbing contractor page, on the other hand, may present challenges. It’s the end of this particular flow, and it needs to cede control back to its parent. This means that this plumbing flow is specific to this form and cannot easily be reused in other flows.
When it's helpful to decouple the branching logic from individual pages, we can use a Decision Node. A Decision Node determines the next page based on data, without being tied to a specific UI component.
const plumbingDecision = {
id: 'great-lakes-decision',
selectNextPage: (data) =>
greatLakeStates.has(data.state)
? 'great-lakes'
: 'plumbing-contractor',
isRequired: () => true,
}
Strengths
- Easy to imagine and explain, since the spec is usually a flowchart.
- Easy to see where the user will go next.
Weaknesses
- It can be harder to reuse sequences, since it's not clear what happens when they are finished.
- Making changes to a flow, such as adding or removing a page, may involve changes to unrelated, distant pages.
- Given two non-sequential pages, it can be hard to know which one will come first.
- It can be easy to accidentally leave pages out of the sequence.
The Flow as a Sequence
Representing this flow as a sequence of pages puts the pages in charge of whether or not they are shown, rather than having pages earlier in the graph determine this.
Although you can't look at a page and see exactly what comes next, you can take a step back and examine the sequence as a whole. Pages become composable, reusable, and changes to the flow are easier to make.
Note that none of these pages have a selectNextPage
method, instead they are ordered.
const plumbingRequirements = {
id: 'plumbing-requirements',
isComplete: (data) => !!data.plumbingRequirements,
}
const greatLakes = {
id: 'great-lakes',
isComplete: (data) => !!data.greatLakes,
// This screen is in charge of showing or hiding itself
isRequired: (data) => greatLakeStates.has(data.state),
Component: GreatLakes,
}
const selectPlumbingContractor = {
id: 'plumbing-contractor',
isComplete: (data) => !!data.plumbingContractor,
}
// To know the order, look at the sequence
const plumbingSequence = {
pages: [
plumbingRequirements,
greatLakes,
selectPlumbingContractor,
],
isRequired: (data) => !!data.requiresPlumbing,
}
In this example, the greatLakes
page uses an isRequired
 function to check if the user’s state is among the Great Lakes states to decide if it should be shown. If it's not shown, the next page that should be shown will be shown.
Strengths
- Easy to get an overview of the process.
- Easy to reuse flows as each page is responsible for its own logic.
- All logic related to a page is very near that page.
- Adding and removing pages is generally very easy.
- Most parts of flows are linear which corresponds to the array model.
Weaknesses
- A larger mental leap from a flowchart to a flat series of pages.
- Looking at a single page, it can be hard to know which one comes next.
- If forms can be filled out in any order (such as plumbing, electrical, construction in the example), it can cause unnatural flows.
When to choose Graph vs. Sequence
Given these two models, how do we decide which one aligns best with our form’s requirements? Is it best to combine the two?
For predominantly linear forms with minimal branching, sequences are often sufficient and more straightforward to implement. However, if your form requires complex conditional navigation or users can navigate sections in any order, incorporating aspects of the graph model can provide the necessary flexibility.
In many cases, combining both approaches yields the best results. You can use sequences for linear sections and integrate decision nodes or graph elements where conditional logic or non-linear navigation is needed.
Non-Linear Forms: Combining Graphs and Sequences
In some forms, users need the flexibility to complete sections in any order. For instance, when selecting contractors for plumbing, electrical, and construction analysis, the order of completion doesn’t matter as long as all required sections are completed.
Summary Page
The summary page acts as a central hub, allowing users to see their progress and navigate to any incomplete sections with these features.
- The user can click any item to start that flow.
- Clicking the continue button goes to the first page that hasn't been completed (
advanceToNextIncompletePage
is provided by React Multi Page Form) - Ideally the sequences are reusable
And we have a page for the summary, the component has a page with links that call goTo
for each section.
const summaryPage = {
id: 'contractor-summary',
isRequired: () => true
isComplete: (data) =>
(!data.plumbingRequired || data.plumbing) &&
(!data.electricalRequired || data.electrical) &&
(data.constructionRequired || data.construction),
Component: ContractorSummary
}
Creating Sequences
Let's imagine that we have created the pages for these flows. Because these are sequences, the pages are implemented sequence style, rather than graph style.
typescript
const plumbingPages = [
plumbingRequirements,
greatLakes,
selectPlumbingContractor,
];
const electricalPages = [
selectElectricalContractor,
getElectricalInsurance
];
const constructionAnalysisPages = [
environmentalImpactAnalysis,
...
permitAndZoning
]
Creating a Decision Node
We need a method that can make the decision if we should go back to the contractor summary after completing a sequence, or if we should continue with the rest of the form. It's made to facilitate this general contracting flow; it's not meant to be used to facilitate other forms, such as a checkout flow specific to plumbing. The decision node will be added to the end of each sequence.
typescript
const decisionNodeBase = {
isRequired: () => true,
selectNextPage: (data) => {
if (data.plumbing && data.electrical && data.construction) {
return 'contractor-confirmation';
}
return 'contractor-summary';
}
}
Adding a Decision Node to the Sequences
const plumbingSequence = {
id: 'plumbing',
isRequired: (data) => isPlumbingRequired(data),
pages: [
// add all the pages of the sequence
...plumbingPages,
// put a decision node at the end
{
id: 'plumbing-return',
...decisionNodeBase
}
]
}
// repeat for electrical and construction
Combining it into a Full Sequence
The sequence below has a few handy features.
- There is an implicit order (plumbing, electrical, construction), so it's easy to provide a "next" button to the user.
- The user will return to the summary after each sequence.
- The user will jump to the end after all sequences are complete.
- This sequence is reusable; after the confirmation page, it will continue to the next page in its parent sequence.
const fullSequence = {
id: 'contractor-selection',
isRequired: (data) => data.requiresContractor,
pages: [
contractorSummary,
plumbingSequence,
electricalSequence,
constructionSequence,
contractorConfirmation
]
}
Testing and Development
The data users enter will guide them on their own unique paths through your forms. Testing these paths is vital to ensure users have a seamless experience and to prevent errors that could disrupt the form flow. Building each form can also be challenging, especially when the form is long and has many possible paths.
React Multi Page Form provides tools to facilitate this process, such as rendering every page of a sequence on a single page and generating lists of visited pages with sample data. These features make it easier to develop, test, and debug complex form flows. See their documentation for more information.
Conclusion
As the processes and forms you build grow in complexity, it’s essential to include tooling not just for UI components, state management, and validation, but also for managing the paths users will take through your forms. By understanding the strengths and weaknesses of graph and sequence models, and knowing when to combine them, you can design flexible and maintainable multi-page forms.
Implementing your sequences using multiple models, depending on your situation, will provide the necessary flexibility and scalability. React Multi Page Form can assist you in this process, offering tools that simplify development and enhance the user experience.
‍