Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing data across Material-UI Stepper using React Hooks

I have a multi-step form that I want to implement in React using Formik, Material-ui, functional components, and the getState hook.

import React, { useState, Fragment } from 'react';
import { Button, Stepper, Step, StepLabel } from '@material-ui/core';
import FormPartA from './FormPartA';
import FormPartB from './FormPartB';
import FormPartC from './FormPartC';

function MultiStepForm(props) {

  const steps = ['Part A', 'Part B', 'Part C'];
  const passedValues = props.values || {};

  const [activeStep, setActiveStep] = useState(0);
  const [values, setValues] = useState({
    field1:(( typeof passedValues.field1 === 'undefined' || passedValues.field1 === null ) ? '1' : passedValues.field1 ),
    field2:(( typeof passedValues.field2 === 'undefined' || passedValues.field2 === null ) ? '2' : passedValues.field2 ),
    field3:(( typeof passedValues.field3 === 'undefined' || passedValues.field3 === null ) ? '3' : passedValues.field3 ),
    field4:(( typeof passedValues.field4 === 'undefined' || passedValues.field4 === null ) ? '4' : passedValues.field4 ),
    field5:(( typeof passedValues.field5 === 'undefined' || passedValues.field5 === null ) ? '5' : passedValues.field5 ),
    field6:(( typeof passedValues.field6 === 'undefined' || passedValues.field6 === null ) ? '6' : passedValues.field6 )
  });

  const handleNext = () => {
    alert({...props.values, ...values});
    setValues({...props.values, ...values});
    setActiveStep(activeStep + 1);
  };

  const handleBack = () => {
    setActiveStep(activeStep - 1);
  };

  function thisStep(step) {
    switch (step) {
      case 0:
        return <FormPartA values={values} setValues={setValues}/>;
      case 1:
        return <FormPartB values={values} setValues={setValues}/>;
      case 2:
        return <FormPartC values={values} setValues={setValues}/>;
      default:
        throw new Error('Mis-step!');
    }
  }

  return (
    <div className="MultiStepForm">
      <Stepper activeStep={activeStep} className={classes.stepper}>
        {steps.map(label => (
          <Step key={label}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      <Fragment>
        {activeStep === steps.length ? ( 
          <p>You're done!<p>
          ) : (
          <Fragment>
            {thisStep(activeStep)}
            <div className={classes.buttons}>
              {activeStep !== 0 && (
                <Button onClick={handleBack} > Back </Button>
              )}
              <Button onClick={handleNext} >
                {activeStep === steps.length - 1 ? 'Done' : 'Next'}
              </Button>
            </div>
          </Fragment>
        )}
      </Fragment>
    </div>
  );
}

Each of the sub-forms, for sake of argument, look roughly like this, with just 2 fields per sub-form:

import React from 'react';

import {Formik, useField, Field, Form} from 'formik';
import { TextField } from 'formik-material-ui';
import * as Yup from "yup";
import { Button } from '@material-ui/core';

export default function BasicForm(props) {

  const field1 = ( typeof props.values.field1 === 'undefined' || props.values.field1 === null ) ? '' : props.values.field1;
  const field2 = ( typeof props.values.field2 === 'undefined' || props.values.field2 === null ) ? '' : props.values.field2;

  return (
    <div>

      <h3>Part A</h3>

      <Formik
        initialValues={{
          field1,
          field2
        }}
        validationSchema={Yup.object({
          field1: Yup.string()
            .required('Required'),
          field2: Yup.string()
            .required('Required'),
        })}
      >
      {({submitForm, isSubmitting, values, setFieldValue}) => (
        <Form>
          <Field name="field1" type="text" label="Field 1" variant="outlined" 
            margin="normal" fullWidth multiline component={TextField} />
          <Field name="field2" type="text" label="Field 2" variant="outlined" 
            margin="normal" fullWidth multiline component={TextField} />
        </Form>
        )}
      </Formik>
    </div>
  );
}

The bit that eludes me is the updating of state. How do I make sure that the child state from each sub-form is saved when stepping between forms? Also, the (( typeof passedValues.field1 === 'undefined' || passedValues.field1 === null ) ? '1' : passedValues.field1 ) construction seems clumsy?

like image 957
Dycey Avatar asked Apr 13 '26 08:04

Dycey


1 Answers

OK, I got it to work, which was lots of fun (for small values of fun). Half the issue was recognising that the activeStep value, handleNext(), and handleBack() functions needed to be passed in to the sub-forms, as well as pre-calculating whether this isLastStep:

import React, { useState, Fragment } from 'react';
import { Button, Stepper, Step, StepLabel } from '@material-ui/core';
import FormPartA from './FormPartA';
import FormPartB from './FormPartB';
import FormPartC from './FormPartC';

const steps = ['Part A', 'Part B', 'Part C'];

function MultiStepForm(props) {

  const { field1, field2, field3, field4, field5, field6, } = props;

  const [activeStep, setActiveStep] = useState(0);
  const [formValues, setFormValues] = useState({
    field1, field2, field3, field4, field5, field6
  });

  const handleNext = (newValues) => {
    setFormValues({ ...formValues, ...newValues });
    setActiveStep(activeStep + 1);
  };

  const handleBack = (newValues) => {
    setFormValues({ ...formValues, ...newValues });
    setActiveStep(activeStep - 1);
  };

  function getStepContent(step) {
    const isLastStep = (activeStep === steps.length - 1);
    switch (step) {
      case 0:
        return <BasicFormA {...formValues} activeStep={activeStep} isLastStep={isLastStep} handleBack={handleBack} handleNext={handleNext}/>;
      case 1:
        return <BasicFormB {...formValues} activeStep={activeStep} isLastStep={isLastStep} handleBack={handleBack} handleNext={handleNext}/>;
      case 2:
        return <BasicFormC {...formValues} activeStep={activeStep} isLastStep={isLastStep} handleBack={handleBack} handleNext={handleNext}/>;
      default:
        throw new Error('Mis-step!');
    }
  }

  return (
    <div className="MultiStepForm">
      <Stepper activeStep={activeStep} className={classes.stepper}>
        {steps.map(label => (
          <Step key={label}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      <Fragment>
        {activeStep === steps.length ? (
           <p>You're done!<p>
        ) : (
        <Fragment> {getStepContent(activeStep)} <Fragment>
        )}
      <Fragment>
    </div>
  );
}

export default MultiStepForm;

At this point, the sub-form can check that its fields are valid, before going to the next step:

import React from 'react';

import {Formik, useField, Field, Form} from 'formik';
import { TextField } from 'formik-material-ui';
import * as Yup from "yup";
import { Button } from '@material-ui/core';

export default function BasicForm(props) {

  const { values, field1, field2, activeStep, isLastStep, handleBack, handleNext } = props;

  return (
    <div>
      <Formik
        initialValues={{
          field1,
          field2
        }}
        validationSchema={Yup.object({
          field1: Yup.string()
            .required('Required'),
          field2: Yup.string()
            .required('Required'),
        })}
      >
      {({submitForm, validateForm, setTouched, isSubmitting, values, setFieldValue}) => (
      <Form>
        <Field name="field1" type="text" label="Field 1" variant="outlined" margin="normal" fullWidth multiline component={TextField} />
        <Field name="field2" type="text" label="Field 2" variant="outlined" margin="normal" fullWidth multiline component={TextField} />
      </Form>
      <div>
        {activeStep !== 0 && (
          <Button onClick={() => { handleBack(values) } } className={classes.button}> Back </Button>
        )}
        <Button className={classes.button} variant="contained" color="primary" 
          onClick={
            () => validateForm()
              .then((errors) => {
                if(Object.entries(errors).length === 0 && errors.constructor === Object ) {
                  handleNext(values);
                } else {
                  setTouched(errors);
                }
              })
          } >
          {isLastStep ? 'Submit Draft' : 'Next'}
        </Button>
      </div>
      )}
    </Formik>
  </div>
  );
}

The only other trick is remembering to setTouched(errors) when the sub-form is invalid, so that untouched fields get their validation errors displayed.

like image 185
Dycey Avatar answered Apr 14 '26 23:04

Dycey



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!