Introduction
Breaking up a form into multiple steps can easily be done with React Hooks and Context. In this tutorial we create a quiz with multiple geographical questions divided in three different steps . Each step needs to be completed before you can move on to the next. Form input must be validated with Yup and form state monitored by React Hooks Form. The code for this project can be found on Github.
See this code in action at CodeSandBox
Why would you like to use form steppers or wizards? Most of all to improve the user experience. Forms are used on all kind of devices, including small screens. Breaking up an extended form is smaller parts does enhance the experience.
Prerequisites
In order to work with the concepts presented in this tutorial you should have a basic understanding of ES6, React hooks, functional components and Context. The project was created with Create-React-App so it is possible to add the code to any React project (check for compatibility though). This tutorial aims to explain how these concepts were used but is not a hands-on tutorial. Please refer to the code on Github.
What is built?
In this tutorial we'll build a form stepper with material-ui@5.4, React@17.x, yup and react-hook-form@7.x.
Our main component is Stepper which imports it's children dynamically, depending on the form step. Each form step should be validated as soon as all fields are touched. If the step is valid the user should be allowed to progress to the next step. All components share state through React Context.
Building the Form Store
Let's start with coding a Context Store. Using a mix of local state and React Context really helps you manage state in any form. Context can be implemented on any level of your application and is perfect for managing form state. Create a folder for our quiz, for instance SelectStepper and code the Context Store:
Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes. So let's import it and wrap it around our form components.
Building the Stepper Coponent
This 'high order component'is basically a Material-UI component that displays progress through a sequence of logical and numbered steps. In this tutorial the code example for a vertical stepper is used which can be viewed here. Basically the code is extended with:
(1) The FormContext store.
(2) Load dynamic content with useEffect hook.
(3) Monitor progress with hook useEffect
So let's import the store and grab the data that should be evaluated when this component (re)renders.
const {
step1Answered,
step2Answered,
finished
} = useContext(FormContext);
Secondly extend the local store so dynamically loaded components can be saved.
const [components, setComponent] = useState({});
const [view, setView] = useState();
We can now use React's useEffect hook to respond to any changed value of activeStep, the variable used to track the current step.
useEffect(() => {
let Component;
const load = async () => {
const StepView = `Step${activeStep+1}`;
if(!components[StepView]) {
const { default:View } = await import(`./Steps/${StepView}`)
Component = <View
FormContext={FormContext}
/>;
setComponent({...components, [StepView]: Component })
setView(Component);
} else {
setView(components[StepView]);
}
}
load();
}, [activeStep]);
This hook function responds to a changed value of the activeStep variable after the component has rendered. It loads any step component from subdirectory Steps synchronous if it is not stored in the components object.
Now edit the HTML so the view is displayed.
<Grid item xs>
<React.Suspense fallback='Loading Form View..'>
{view}
</React.Suspense>
</Grid>
React hook useEffect is used to respond to data changes after a component has rendered. It is basically triggered whenever one of the values of it's deps array changes.
If you use useEffect without dependencies (or an empty array) it will only run once after the initial render.
Thirdly let's add a hook function that responds when the user moves from step to step or answered all questions.
useEffect(() => {
setSolutionProvided(false);
if (activeStep === 0 && step1Answered) {
setSolutionProvided(true);
}
if (activeStep === 1 && step2Answered) {
setSolutionProvided(true);
}
if (activeStep === steps.length - 1 && finished) {
setSolutionProvided(true);
}
}, [activeStep, step1Answered, step2Answered, finished]);
Local state variable solutionProvided can now be used to control the state of the 'Next' Button.
<Button
variant="contained"
disabled={!solutionProvided }
onClick={() => handleNext(activeStep, steps)}
>
{activeStep === steps.length - 1 ? 'Save' : 'Next'}
</Button>
Building the Step Forms
Form Element
Let's now add the formsteps which use a single form element, Material-UI Select, wrapped in the Controller wrapper component of React Hook Form. This component makes it easier to work with external controlled components such as Material-UI.
The render prop is a function that return a React element so events can be attached. The onChange function shall be used to evaluate a selected value in the parent component.
The Step Form
To create a form the following steps have to be coded:
- Set up Yup Form Schema with react-hook-form
- Load values from the Context Store if the user filled out the form previously
- Evaluate User Input
- Store step result
Set up Yup Form Schema with react-hook-form
Yup provides advanced methods for validation. As this form with Material-UI selects you can for instance test if the selected value is > 0 or in range [ 0, (options.length + 1)]. React-hook-form needs initial values for the form fields it controls.
const formSchema = yup.object().shape({
.....
})
let formValues = {
...
}
Inside the form component:
javascript
const {
register,
watch,
setValue,
getValues,
control,
formState: {
isValid
}
} = useForm({
formValues,
resolver: yupResolver(formSchema)
});
const formFields = watch();
The variable formFields, created with the watch of react-hook-form is now subscribed to all input changes. As soon as all form elements are validated by Yup - isValid property of formState - this input can be compared with the required solution on every render.
Load values from the Context Store
For this use the useEffect hook without dependencies or an empty array.
useEffect(() => {
implementSolution();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
To retrieve data from the form store the useCallback hook is used.
const implementSolution = useCallback(() => {
// retrieve stored values from Context
// assign values to controlled elements
// assign values to local state
}, [data, setValue, stepSolution]);
Local state is used to initialize the form elements. For instance:
const [defaultValue, setDefaultValue] = useState(0);
<SelectBox
...
value={defaultValue}
/>
Evaluate User Input
After each render this hook function destructures first all form fields, sets their values in the local store, evaluates if all fields have been touched hich leads to evaluation of user's input.
useEffect(() => {
const {
// fields
} = formFields;
// update local store with form values
// Were all fields validated? Then evaluate input and enable
// next step if needed
if (isValid) {
// evaluate user input
const solutionProvided = getSolution();
setStepAnswered(solutionProvided);
}
}, [
formFields,
isValid,
getSolution()
...
]);
The getSolution() uses a useCallback hook and the getValues method of react-hook-form.
const getSolution = useCallback(values => {
const guess = getValues();
const solution = (
// your condition
// set step answered
);
return (solution) ? true : false;
}, [getValues]);
Store step result
Finally create a useEffect hook function that responds to a changed value of variable stepAnswered which should store all fprm step values in the Context Store.
useEffect(() => {
// save form data
}, [StepAnswered]
Example of a functional form component with all these steps combined:
More examples can be found in the repo.
Summary
This was just a basic example of a Material-UI form wizard (stepper). It's just the tip of the iceberg: with React Hook Form you could change a single form component into another form wizard by using (nested) routes.
Top comments (1)
@negreirost , olha isso.