Turn any Form into a stepper form wizard with UI, Hooks, Context, React-Hook-Form and Yup

paulvermeer2021

full-stack-concepts

Posted on February 27, 2022

Turn any Form into a stepper form wizard with UI, Hooks, Context, React-Hook-Form and Yup

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.

Screenshot of form

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

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);


Enter fullscreen mode Exit fullscreen mode

Secondly extend the local store so dynamically loaded components can be saved.



    const [components, setComponent] = useState({});
    const [view, setView] = useState();    


Enter fullscreen mode Exit fullscreen mode

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]); 


Enter fullscreen mode Exit fullscreen mode

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> 


Enter fullscreen mode Exit fullscreen mode

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]);


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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:

  1. Set up Yup Form Schema with react-hook-form
  2. Load values from the Context Store if the user filled out the form previously
  3. Evaluate User Input
  4. 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 = {
   ...
}


Enter fullscreen mode Exit fullscreen mode

Inside the form component:


 javascript
  const {
        register,
        watch,
        setValue,
        getValues,
        control,
        formState: {
            isValid
        }
    } = useForm({
        formValues,
        resolver: yupResolver(formSchema)
    });

  const formFields = watch();


Enter fullscreen mode Exit fullscreen mode

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
}, []);


Enter fullscreen mode Exit fullscreen mode

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]);


Enter fullscreen mode Exit fullscreen mode

Local state is used to initialize the form elements. For instance:



  const [defaultValue, setDefaultValue] = useState(0);


Enter fullscreen mode Exit fullscreen mode


  <SelectBox
    ...
    value={defaultValue}            
  />


Enter fullscreen mode Exit fullscreen mode

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()
  ...
]);


Enter fullscreen mode Exit fullscreen mode

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]);


Enter fullscreen mode Exit fullscreen mode

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]


Enter fullscreen mode Exit fullscreen mode

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.

đź’– đź’Ş đź™… đźš©
paulvermeer2021
full-stack-concepts

Posted on February 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related