I decided to invest my time into covering the edge cases that were not handled by Design #1. Refer to yesterday’s post for the context. The edge cases are:
- Forms which need data from the preceding forms
- Dynamic form links
I can deal with the first pain point by leveraging query parameters. A persistent Redux store is not needed (yet). A persistent Redux store is one which persists between page loads and refreshes.
On the other hand, building dynamic form links for step navigation is still a huge headache for me.
This is how you configure MultiStepFormPageTemplate
in the routing configuration file:
// Warning: I wrote these lines without any help from the compiler.
const stepLinks = [
[PageComponent1, '/path1'],
[PageComponent2, '/path1/:param1'],
[PageComponent3, '/path1/:param1/path2'],
[PageComponent4, '/path1/:param1/path2/path3'],
[PageComponent5, '/path1/:param1/path4/:param2'],
];
return (
{stepLinks && stepLinks.map(([component, stepLink], stepNumber) => {
return (
<MultiStepFormPageTemplate
path={stepLink} // A Reach router prop
stepPage={component}
stepNumber={stepNumber}
stepLinks={stepLinks}
/>
);
})}
);
You should have noticed stuff like :param1
and :param2
. These are dynamic at runtime and can change based on user interaction. It isn’t possible to retrieve the preceding link (also known as a HTTP referer) (e.g. navigating from /path1/:param1/path2/:param2
to /path1/:param1
) without hardcoding knowledge about the current link (e.g. /path1/:param1/path2/:param2
). As such, I will need to either:
- Store the referer in a persistent Redux store. I may need to investigate Redux middlewares to do this.
- Move
:param1
and:param2
to query parameters. Then, access the query string with Reach router’suseLocation().search
. However, there are existing links which have:param1
and:param2
. I don’t want to break existing implementations.
It seems I don’t have much a choice other than investigating Redux middlewares to make Redux storage persistent so that I can store the referers. If I can persist the Redux storage, I would want to cache the user’s input from the previous forms as well so that he/she can navigate to a form containing his/her recent response.
These concerns look like things that a multi-step form library should have covered. Unfortunately, frontend components are not obliged to provide out-of-the-box state management.
The multi-step component libraries I know of in the wild are not suitable. They compel me to conduct an overhaul and/or major copypasta of the current codebase. For instance, the libraries can come with their own frontend components. Users will need to reorganise the form into a JSON object. I used one a few years ago, for a closed source codebase. It was crazy - bulky and many LoC.
I don’t want to give up.
There isn’t much development to MultiStepFormPageTemplate
. I created an additional prop interface and preserved query parameters. Here’s the code:
import React, { FC } from 'react';
import { navigate, RouteComponentProps, useLocation } from '@reach/router';
// Form component props must extend from HasRedirectOnSubmit
export interface HasRedirectOnSubmit extends RouteComponentProps {
nextStepLink?: string;
}
interface Props extends RouteComponentProps {
stepPage: FC<HasRedirectOnSubmit>;
stepNumber: number;
stepLinks: string[];
}
const hasPrevStep = (stepNumber: number) => {
return stepNumber > 0;
};
const hasNextStep = (stepNumber: number, steps: string[]) => {
return stepNumber < steps.length - 1;
};
const getNextStepLink = (stepNumber: number, steps: string[]) => {
if (hasNextStep(stepNumber, steps)) {
return steps[stepNumber + 1];
}
return '';
};
const goToStep = (stepLink: string) => {
return async (e: { preventDefault: () => void }) => {
if (stepLink) {
await navigate(stepLink);
}
};
};
export const MultiStepFormPageTemplate: FC<Props> = (props: Props) => {
// These lines preserve the query params.
const queryString = useLocation().search;
const previousStepLink = props.stepLinks[props.stepNumber - 1] + queryString;
const nextStepLink = getNextStepLink(props.stepNumber, props.stepLinks) + queryString;
return (
<div className="multi-step-form">
<div className="progress-bar"></div>
<div className="form-page">
<props.stepPage nextStepLink={nextStepLink} />
</div>
<div className="step-navigation">
<div className={hasPrevStep(props.stepNumber) ? 'show' : 'hide'}>
<button onClick={goToStep(previousStepLink)}>
Previous
</button>
</div>
<div className={hasNextStep(props.stepNumber, props.stepLinks) ? 'show' : 'hide'}>
<button onClick={goToStep(nextStepLink)}>
Next
</button>
</div>
</div>
</div>
);
};