Turn any Form into a stepper form wizard with UI, Hooks, Context, React-Hook-Form and Yup
Published: 2022-02-27 13:52:43

**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](https://github.com/full-stack-concepts/react-form-stepper-select).

[See this code in action at CodeSandBox](https://codesandbox.io/s/agitated-mcnulty-c1bxd8?file=/src/components/SelectStepper/Stepper.js)

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](https://github.com/full-stack-concepts/react-form-stepper-select). 

**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. 


![Screenshot of form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irp3lxrnelf0ew6vq23e.png)

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.


![Components](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tpyz87b9l2aokx34x0lg.png)

**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 <em>SelectStepper</em> and code the Context Store:

{% gist 
https://gist.github.com/full-stack-concepts/f709d106a46349adce71f7f0f9aa8778 
%}

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.

{% gist 
https://gist.github.com/full-stack-concepts/1827e2161faac94eaed472ca93fe7c23 
%}

**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](https://mui.com/components/steppers/#vertical-stepper). 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.

```javascript
    const {
        step1Answered,
        step2Answered,
        finished
    } = useContext(FormContext);
```
Secondly extend the local store so dynamically loaded components can be saved.

```javascript
    const [components, setComponent] = useState({});
    const [view, setView] = useState();    
```
We can now use React's <em>useEffect</em> hook to respond to any changed value of <em>activeStep</em>, the variable used to track the current step.

```javascript
   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 <em>activeStep</em> variable after the component has rendered. It loads any step component from subdirectory Steps synchronous if it is not stored in the <em>components</em> object.

Now edit the HTML so the view is displayed.
```html
<Grid item xs>                              
  <React.Suspense fallback='Loading Form View..'>
    {view}     
  </React.Suspense>                          
</Grid> 
```

React hook <em>useEffect</em> 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.

```javascript
    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 <em>solutionProvided</em> can now be used to control the state of the 'Next' Button.

```html
<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 <em>[Select](https://mui.com/components/selects/)</em>, wrapped in the [Controller wrapper component](https://react-hook-form.com/api/usecontroller/controller/) of React Hook Form. This component makes it easier to work with external controlled components such as Material-UI.

{% gist 
https://gist.github.com/full-stack-concepts/bcfb19d5d30c085b11dcb2a7a3dca659
%}

The render prop is a function that return a React element so events can be attached. The <em>onChange</em> 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:
<ol>
  <li>Set up Yup Form Schema with react-hook-form</li>
  <li>Load values from the Context Store if the user filled out the form previously</li>  
  <li>Evaluate User Input</li>
<li>Store step result</li>
</ol>

<em>**Set up Yup Form Schema with react-hook-form**</em>
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.

```javascript
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 <em>formFields</em>, created with the <em>watch</em> 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](https://react-hook-form.com/api/useform/formstate) - this input can be compared with the required solution on every render.

**<em>Load values from the Context Store</em>**
For this use the <em>useEffect</em> hook without dependencies or an empty array.

```javascript
useEffect(() => {
  implementSolution();
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
```

To retrieve data from the form store the [useCallback](https://reactjs.org/docs/hooks-reference.html) hook is used. 

```javascript
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:
```javascript
  const [defaultValue, setDefaultValue] = useState(0);
```
```html
  <SelectBox
    ...
    value={defaultValue}            
  />
```

**<em>Evaluate User Input</em>**
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. 

```javascript
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 <em>getSolution()</em> uses a [useCallback](https://reactjs.org/docs/hooks-reference.html) hook and the <em>getValues</em> method of react-hook-form.

```javascript
const getSolution = useCallback(values => {
  const guess = getValues();
  const solution = (
    // your condition
    // set step answered
  );
  return (solution) ? true : false;
    }, [getValues]);
```
**<em> Store step result </em>**
Finally create a <em>useEffect</em> hook function that responds to a changed value of variable <em>stepAnswered</em> which should store all fprm step values in the Context Store.

```javascript
useEffect(() => {
  // save form data
}, [StepAnswered]
```
Example of a functional form component with all these steps combined:

{% gist 
https://gist.github.com/full-stack-concepts/741e7b9bc18a6a2bdb4f5d2a9dbd6814
%}

More examples can be found in the [repo](https://github.com/full-stack-concepts/react-form-stepper-select).

**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](https://react-hook-form.com/advanced-usage/#WizardFormFunnel).