Trying to refactor the onSubmit property using Formik

Viewed 540

Brushing up my development skills with React. I'm trying to figure a way to refactor the onSubmit property. My application is a contact form using the Formik component which sends the data to Firebase Cloudstore as well as sending an email via emailjs. If it's a success, it'll have a popup using Material UI's Snackbar. It works, but just trying to clean up the code. Please help!

onSubmit={(values, { resetForm, setSubmitting }) => {          
          emailjs.send("blah","blah", {
            email: values.email,
            name: values.name,
            message: values.message
            }, 
            'blah',);    

          //this is sent to firebase cloudstore
          db.collection("contactForm")
            .add({
              name: values.name,
              email: values.email,
              message: values.message,
            })
            .then(() => {
              handleClick();
            })
            .catch((error) => {
              alert(error.message);
            });
          setTimeout(() => {
            resetForm();
            setSubmitting(false);
            /*    console.log(values);
            console.log(JSON.stringify(values, null, 2)); */
          }, 500);
        }}

Here's the complete function

function Contact() {
  const [open, setOpen] = React.useState(false);
  const handleClose = (event, reason) => {
    if (reason === "clickaway") {
      return;
    }
    setOpen(false);
  };
  const handleClick = () => {
    setOpen(true);
  };
  const classes = useStyles();
  return (
      <Formik
        initialValues={initialValues}
        validationSchema={validationSchema}
        onSubmit={(values, { resetForm, setSubmitting }) => {          
          emailjs.send("blah","blah", {
            email: values.email,
            name: values.name,
            message: values.message
            }, 
            'blah',);    

          //this is sent to firebase cloudstore
          db.collection("contactForm")
            .add({
              name: values.name,
              email: values.email,
              message: values.message,
            })
            .then(() => {
              handleClick();
            })
            .catch((error) => {
              alert(error.message);
            });
          setTimeout(() => {
            resetForm();
            setSubmitting(false);
            /*    console.log(values);
            console.log(JSON.stringify(values, null, 2)); */
          }, 500);
        }}
      >
        {({ submitForm, isSubmitting }) => (
          <Form>
            <Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
              <Alert onClose={handleClose} severity="success">
                Your message has been sent!
              </Alert>
            </Snackbar>

            <div>
              <Field
                component={TextField}
                label="Name"
                name="name"
                type="name"
              />
              <ErrorMessage name="name" />
            </div>

            <div>
              <Field
                component={TextField}
                label="Your email"
                name="email"
                type="email"
              />
              <ErrorMessage name="email" />
            </div>
            <br />
            <br />
            <div>
              <Field
                as="textarea"
                placeholder="Your Message"
                label="message"
                name="message"
                type="message"
                rows="15"
                cols="70"                
              />
              <ErrorMessage name="message" />
            </div>

            {isSubmitting && <LinearProgress />}

            <Button
              variant="contained"
              color="primary"
              disabled={isSubmitting}
              onClick={submitForm}
            >
              Submit
            </Button>
          </Form>
        )}
      </Formik>
  );
}
1 Answers

I would recommend making the onSubmit property it's own function in the component body, you will want to memoize this using useCallback. Additionally, you can create a hook to allow you to control the alert component, you can also allow the hook to control weather it's an error or success type, reducing the need to duplicate code if it fails to save.

Your submission handler could look like this, note that I omitted the sending of the email and mocked the firebase portion. Also you can call finally on the promise, rather than calling setSubmitting in both the then and catch blocks.

  const handleSubmit = React.useCallback(
    (values, { setSubmitting, resetForm }) => {
      db.collection("contact")
        .add(values)
        .then((res) => {
          show({ message: "Your message has been sent" });
        })
        .catch((err) => {
          show({ variant: "error", message: "Failed to send your message." });
        })
        .finally(() => {
          setSubmitting(false);
        });
    },
    [show]
  );

The show function in the above example would be part of your hook to control the alert. The hook could look something like this, it could be extended based on your usecase.

import React from "react";

const useAlert = () => {
  const [state, setState] = React.useState({
    variant: "success",
    visibile: false,
    message: null
  });

  const show = React.useCallback(
    (options = {}) => {
      setState((prev) => ({
        ...prev,
        ...options,
        visible: true
      }));
    },
    [setState]
  );

  const hide = React.useCallback(() => {
    setState((prev) => ({ ...prev, visibile: false }));
  }, [setState]);

  return { ...state, show, hide };
};

export default useAlert;

Additionally, since you're using material ui, you'll want to take advantage of their built in components. This would remove the need for your multiple <br />s for spacing, as well as help to keep the UI consistent.

<Box marginBottom={1}>
  <Field component={TextField} label="Name" name="name" type="name" />
  <ErrorMessage name="name" />
</Box>

<Box marginBottom={1}>
  <Field
    component={TextField}
    label="Email"
    name="email"
    type="email"
  />
  <ErrorMessage name="email" />
</Box>

Also, you could use the built in component for the text area, keeping the design consistent. Using the multiline prop allows you to make the input a text area.

<Box marginBottom={2}>
  <Field
    component={TextField}
    placeholder="Your Message"
    label="Message"
    name="message"
    type="message"
    rows={5}
    multiline
    fullWidth
  />
  <ErrorMessage name="message" />
</Box>

I'm personally not a huge fan of using the LinearProgress in the manner than your did. I personally think that the circular process looks better, specifically when used inside the submit button. Here are the relevant docs.

<Button
  variant="contained"
  color="primary"
  disabled={isSubmitting}
  onClick={submitForm}
  endIcon={isSubmitting && <CircularProgress size={15} />}
>
  Submit
</Button>

I've put a working example together in a codesandbox.

Related