ReactT Light_870x220

Form Development in React comes down to three things: Data, Validations, and Submission. See how to handle these yourself or using Formik to make things simpler.

As your form in React becomes more complicated, you will find yourself reinventing more and more of the functionality that comes with Formik. If you find manually controlling a form and its validation painful, it may be time to switch to Formik or another form package to help make this process a bit easier to manage. In this article we'll investigate forms in Vanilla React and compare that to forms with Formik. 

When you’re thinking of forms in React, there are three things to keep in mind:

  1. How do I access what the user entered?
  2. How do I ensure what they entered is valid?
  3. How do I submit their data to the server?

That order is important because you can’t do step two unless you have first done step one, and you wouldn’t want to submit invalid data to the server.

This article will show the basics of Forms in React, both with and without the help of additional packages. We’ll see how to do “Vanilla React Forms”, and then how to accomplish the same thing using the Formik package from Jared Palmer.

My thesis is that the simpler the form, the more you can lean on React without additional packages, but as the number of fields increases and the validations get trickier, we’ll tend to stick with Formik to avoid rebuilding Formik ourselves!

All examples in their entirety can be found here on GitHub.

Vanilla React Forms

When I say “Vanilla React Forms,” I am referring to nothing else other than React… no additional packages. As you’ll see in this section, it could start to get out of control pretty quickly, as with just a single input that has some validations, it’s already turning into a decent-sized component.

Controlled Components

To answer “How do I access what the user entered?” we will use Controlled Components. Controlled Components are where the user’s input will trigger an update to the component’s state, which will cause a re-render of the component, displaying what the user entered.

By using the onChange event on an input field, we can update the state. Then, having the value prop equal to the value in our state, we can display it to the user.

export default function Controlled() {
  const [value, setValue] = React.useState("");

  return (
    <form>
      <input
        type="text"
        placeholder="Controlled Name"
        onChange={event => setValue(event.target.value)}
        value={value}
      />
    </form>
  );
}

Validating Data

To validate our user’s input, we’ll maintain an object of errors in our state. This will get populated any time the user changes a value in the form and prior to the form’s submission. Leaving aside form submission for now, let’s look at the validate function. It will start fresh every time, populating an errors object based on the current values in our form.

function validate(values) {
  let errors = {};

  if (!values.name) {
    errors.name = "Required";
  }

  return errors;
}

Using the useEffect hook, we can detect when any of the input values change, calling the validate function and placing its result into our state. With an errors object, we can optionally add a class to our input field by looking to see if the field has an error: className={errors.name ? "has-error" : null}. Below the input field, we pass the error message to a component called Error which will render the message (if it exists) into an element with the correct classes.

export default function VanillaForm() {
  const [submitting, setSubmitting] = React.useState(false);
  const [name, setName] = React.useState("");
  const [errors, setErrors] = React.useState({});

  // Recalculate errors when any of the values change
  React.useEffect(() => {
    setErrors(validate({ name }));
  }, [name]);

  return (
    <form
      onSubmit={event => {
        event.preventDefault();
      }}
    >
      <h2>An Average Form</h2>

      <div className="input-row">
        <label>Name</label>
        <input
          type="text"
          name="name"
          onChange={event => {
            setName(event.target.value);
          }}
          value={name}
          className={errors.name ? "has-error" : null}
        />
        <Error message={errors.name} />
      </div>

      <div className="input-row">
        <button type="submit" disabled={submitting}>
          Submit
        </button>
      </div>
    </form>
  );
}

Submitting Data

Finally, with our input value inside of name and the validation handled, it’s time to submit the form. A normal HTML form uses the form’s action prop, containing a URL to POST the data to, but in this case we will use the form’s onSubmit event to take matters into our own hands.

In order to stop the form from submitting via the normal method, we’ll call event.preventDefault(). Just to ensure our validation is completely up to date, we can call the validate check one last time. After that, it’s just a matter of posting the data somewhere using fetch, Axios, or perhaps with a mutation in GraphQL. In this case we’ll alert the data so we can see it in the browser.

event => {
  // Stop the form from submitting
  event.preventDefault();

  // Validate the data one last time
  if (Object.keys(validate({ name })).length > 0) {
    return;
  }

  // Update the submitting state to true
  setSubmitting(true);

  // Time to process the data
  setTimeout(() => {
    const values = { name };
    alert(JSON.stringify(values, null, 2));
    setSubmitting(false);
  }, 500);
};

Formik

For more complicated forms - perhaps with multiple fields or validations - it’s time to reach for a package called Formik. The principles are the same as we covered above, but it handles a lot of the heavy lifting for us. In this form, we’ll consider some more advanced use cases, including conditionally displaying fields and validating them, based on a value from an Autosuggest field.

In order to focus on the functionality we are discussing, I am going to slice and dice this somewhat large component to show what is important to the specific example. You can find the entire component here.

Accessing Data

Formik provides us with a values object. It gets its initial values using the initialValues prop, and then is updated automatically by the onChange event on each individual field. An important thing to keep in mind is that Formik uses the name prop of each input to know which value to set.

export default function FormikForm() {
  return (
    <Formik
      initialValues={{
        name: "",
        email: "",
        country: "",
        postalCode: ""
      }}
    >
      {({
        values,
        errors,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting,
        setFieldValue
      }) => (
        <form onSubmit={handleSubmit}>
          <h2>A Great Form</h2>

          <div className="input-row">
            <label>Name</label>
            <input
              type="text"
              name="name"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.name}
              className={errors.name ? "has-error" : null}
            />
            <Error message={errors.name} />
          </div>

          {/* Additional fields here */}

          <div className="input-row">
            <button type="submit" disabled={isSubmitting}>
              Submit
            </button>
          </div>
        </form>
      )}
    </Formik>
  );
}

Validating Data

Formik provides two main ways to validate user data: The first approach requires us to populate an errors object, similar to how it was done in the Vanilla React examples. The second approach uses Yup to define a validation schema, handling validation in a structured and simple way.

const ValidationSchema = Yup.object().shape({
  name: Yup.string()
    .min(1, "Too Short!")
    .max(255, "Too Long!")
    .required("Required"),
  country: Yup.string()
    .min(1, "Too Short!")
    .max(255, "Too Long!")
    .required("Required"),
  email: Yup.string()
    .email("Must be an email address")
    .max(255, "Too Long!")
    .required("Required")
});

With our validation schema in place, we can pass it to the Formik component. At the same time, we’ll pass a function to the validate prop so we can add errors ourselves when Yup doesn’t cut it. This will be explained in further detail when we discuss conditional fields.

<Formik
  validationSchema={ValidationSchema}
  validate={values => {
    let errors = {};

    // Validate the Postal Code conditionally based on the chosen Country
    if (!isValidPostalCode(values.postalCode, values.country)) {
      errors.postalCode = `${postalCodeLabel(values.country)} invalid`;
    }

    return errors;
  }}
>
  {/* Fields here... */}
</Formik>

Errors are then accessed with the errors object passed via the render prop function. You can see how they are used to add a class to the input and display errors below:

<div className="input-row">
  <label>Name</label>
  <input
    type="text"
    name="name"
    onChange={handleChange}
    onBlur={handleBlur}
    value={values.name}
    className={errors.name ? "has-error" : null}
  />
  <Error message={errors.name} />
</div>

Autosuggest with Formik

A common use case when building a form is to have an autosuggest/autocomplete field, where, as you type, the suggested values are displayed below for the user to select. For this we’ll use react-autosuggest. The field will allow the user to search from a list of countries (retrieved from a JSON feed).

In this case we won’t update our Formik country value as the user types each character, but instead set it ourselves using the setFieldValue function. This means that Formik is only aware of the country value when the user selects a suggestion. The react-autosuggest package requires us to control the input values, so we’ll declare country and suggestions state values.

Before looking at the entire example, we’ll see what happens when a user makes a selection. Using the onSuggestionSelected prop, we can call setFieldValue:

(event, { suggestion, method }) => {
  // Stop form from submitting by preventing default action
  if (method === "enter") {
    event.preventDefault();
  }
  // Update country state, this is used by us and react-autosuggest
  setCountry(suggestion.name);
  // Update country value in Formik
  setFieldValue("country", suggestion.name);
};

Note that when the “method” (how the suggestion was selected) equals “enter,” we’ll prevent default for this event, because otherwise the form will be submitted, when the user just wanted to select a suggestion.

Below we have the full example, which may seem rather long, but there are a number of props that control how the suggestions are fetched and then rendered. Notice that I still use errors provided by Formik. Because of our use of setFieldValue, Formik will view it as invalid until the user selects a suggestion from the list.

export default function FormikForm() {
  const [country, setCountry] = React.useState("");
  const [suggestions, setSuggestions] = React.useState([]);

  return (
    <Formik>
      {({
        values,
        errors,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting,
        setFieldValue
      }) => (
        <form onSubmit={handleSubmit}>
          <div className="input-row">
            <label>Country</label>
            <Autosuggest
              suggestions={suggestions}
              onSuggestionsFetchRequested={async ({ value }) => {
                // An empty value gets no suggestions
                if (!value) {
                  setSuggestions([]);
                  return;
                }

                // Try to populate suggestions from a JSON endpoint
                try {
                  const response = await axios.get(
                    `https://restcountries.eu/rest/v2/name/${value}`
                  );

                  setSuggestions(
                    response.data.map(row => ({
                      name: row.name,
                      flag: row.flag
                    }))
                  );
                } catch (e) {
                  setSuggestions([]);
                }
              }}
              onSuggestionsClearRequested={() => {
                setSuggestions([]);
              }}
              getSuggestionValue={suggestion => suggestion.name}
              renderSuggestion={suggestion => <div>{suggestion.name}</div>}
              onSuggestionSelected={(event, { suggestion, method }) => {
                if (method === "enter") {
                  event.preventDefault();
                }
                setCountry(suggestion.name);
                setFieldValue("country", suggestion.name);
              }}
              inputProps={{
                placeholder: "Search for your country",
                autoComplete: "abcd",
                value: country,
                name: "country",
                onChange: (_event, { newValue }) => {
                  setCountry(newValue);
                },
                className: errors.country ? "has-error" : null
              }}
            />
            <Error message={errors.country} />
          </div>
        </form>
      )}
    </Formik>
  );
}

Conditional Fields

Now that the user has chosen their country from the autosuggest list, we will optionally display a Postal Code field. Due to “budgetary restrictions,” our boss only wants to show this field to users from USA and Canada. Because the US uses ZIP Code, and Canada uses Postal Code, each with their own set of validation rules, we’ll be using the country value to determine which label to display and which validation rule to use.

I have found Yup perfect for straightforward “fixed” validations, but in this case it made sense to handle validations ourselves in Formik:

function isValidPostalCode(postalCode, country) {
  let postalCodeRegex;

  switch (country) {
    case "United States of America":
      postalCodeRegex = /^([0-9]{5})(?:[-\s]*([0-9]{4}))?$/;
      break;
    case "Canada":
      postalCodeRegex = /^([A-Z][0-9][A-Z])\s*([0-9][A-Z][0-9])$/;
      break;
    default:
      return true;
  }
  return postalCodeRegex.test(postalCode);
}

function postalCodeLabel(country) {
  const postalCodeLabels = {
    "United States of America": "Zip Code",
    Canada: "Postal Code"
  };
  return postalCodeLabels[country] || "Postal Code";
}

function showPostalCode(country) {
  return ["United States of America", "Canada"].includes(country);
}

export default function FormikForm() {
  return (
    <Formik
      validationSchema={ValidationSchema}
      validate={values => {
        let errors = {};

        // Validate the Postal Code conditionally based on the chosen Country
        if (!isValidPostalCode(values.postalCode, values.country)) {
          errors.postalCode = `${postalCodeLabel(values.country)} invalid`;
        }

        return errors;
      }}
    >
      {({
        values,
        errors,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting,
        setFieldValue
      }) => (
        <form onSubmit={handleSubmit}>
          {showPostalCode(values.country) ? (
            <div className="input-row">
              <label>{postalCodeLabel(values.country)}</label>
              <input
                type="text"
                name="postalCode"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.postalCode}
                className={errors.postalCode ? "has-error" : null}
              />
              <Error message={errors.postalCode} />
            </div>
          ) : null}
        </form>
      )}
    </Formik>
  );
}

Submitting Data

Formik provides us with an onSubmit prop to handle form submission. We don’t have to “prevent default” like we did when managing this directly ourselves, and instead we are provided with all of the form’s values, along with a function called setSubmitting to control a Boolean value of whether or not the form is being submitted, and resetForm to set the form back to its initial state.

(values, { setSubmitting, resetForm }) => {
  setSubmitting(true);

  setTimeout(() => {
    alert(JSON.stringify(values, null, 2));
    resetForm();
    setCountry("");
    setSubmitting(false);
  }, 500);
};

Conclusion

Forms in React — when you strip everything else away — involve the onSubmit event on the form element and the onChange event on each individual input. As your form becomes more complicated, you will find yourself reinventing more and more of the functionality that comes with Formik. If you find manually controlling a form and its validation painful, it may be time to switch to Formik or another form package to help make this process a bit easier to manage.

Keep Reading

Keep learning about Formik with this next post, Build Better React Forms with Formik.


leigh-halliday
About the Author

Leigh Halliday

Leigh Halliday is a full-stack developer specializing in React and Ruby on Rails. He works for FlipGive, writes on his blog, and regularly posts coding tutorials on YouTube.

Related Posts

Comments

Comments are disabled in preview mode.