React Forms Then and Now: Using Hooks for Reusable Form Logic

Christian Nwamba

·

March 27, 2019

The useState hook makes it easier to reuse state logic. useState is a React Hook. Is there a way to reuse other React-related logic using hooks, just like we do for state using the useState hook? Well, that is why we have custom hooks. With custom hooks, we can repurpose common React patterns and make them reusable through a neat hook API.

The goal of this article is that you should be able to build simpler forms as shown in the video below:

One of these patterns is found within the forms realm. How can we reduce writing handlers, validators, etc for each field? If we can reuse state logic, how about form logic?

Let’s start with fundamentals.

Controlled vs. Uncontrolled

There are two conventional ways to manage form states in React. The first one takes state management control from the form fields and lifts the control to React state using setState. The second leaves state management to the form fields but extracts the values using React’s ref.

The first option is referred to as Controlled and the second option is known as Uncontrolled. We won’t be spending time trying to understand their differences, but the table below from Gosha’s post helps:

Well obviously, controlled is preferable. It also makes it easier for you to reuse form logic which is basically what this post is all about.

Handling Forms in Controlled Components

I want to start by showing you how most React developers handle forms in controlled components. Feel free to skip this section if you already have enough experience building forms in React. You can follow along by forking the CodeSandbox below. It is just a styled form template that lives in a React component but has no working logic wired yet:

We want to be able to keep track of the values in the form so that we can submit them when the submit button is clicked. To keep track of values in a controlled component, we just need to update the state every time a field is updated.

For each field add a state property to represent it:

class App extends React.Component {
  state = {
    name: "",
    email: "",
    meal: "",
    isGoing: ""
  };

  render() {...}
}

Then we can pass the state items to each of the field through the value prop:

<input type="text" name="name" value={this.state.name} />

<input type="email" name="email" value={this.state.email} />

<select name="meal" value={this.state.meal}>
 ...
</select>
<input name="isGoing" type="checkbox" value={this.state.isGoing} />

Notice how this change breaks the form. If you’re not a React beginner, pardon me — I’m just empathizing.

The form fields are now broken because we have stolen the state control away from the fields and given it to the component. We did this by setting the values prop to the component’s state items. If we are going to take away state handling from the fields, we also need to take away change handling from it — which it was doing before we decided to control (pun intended) the values.

To take control of change handling, add a handler to the onChange event to each field:

<input
  type="text"
  name="name"
  value="{this.state.name}"
  <!-- highlight -->
  onChange="{this.handleNameChange}"
/>
<input
  type="email"
  name="email"
  value="{this.state.email}"
  <!-- highlight -->
  onChange="{this.handleEmailChange}"
/>
<select
  name="meal"
  value="{this.state.meal}"
  <!-- highlight -->
  onChange="{this.handleMealChange}"
>
</select>
<input
  name="isGoing"
  type="checkbox"
  value="{this.state.isGoing}"
  <!-- highlight -->
  onChange="{this.handleIsGoingChange}"
/>

Then create the handlers as class methods which will take the values from each field and update the state using setState. All the fields have their value stored in event.target.value excluding checkboxes. A checkbox value is boolean and stored in event.target.checked:

handleNameChange = event => {
  this.setState({ name: event.target.value });
};

handleEmailChange = event => {
  this.setState({ email: event.target.value });
};

handleMealChange = event => {
  this.setState({ meal: event.target.value });
};

handleIsGoingChange = event => {
  this.setState({ isGoing: event.target.checked });
};

To see the everything in action, you can attach a submit handler to the form:

<form onSubmit={handleSubmit}>...

We can simply just alert the content of the state on submit:

handleSubmit = event => {
  event.preventDefault();
  alert(JSON.stringify(this.state, null, 2));
};

You should see a neatly formatted window alert pop out:

You can see for yourself in the demo:

Clever Refactor with One Handler

If you are obsessed with clean and reusable code, this code snippet we saw above should spoil your day:

handleNameChange = event => {
  this.setState({ name: event.target.value });
};

handleEmailChange = event => {
  this.setState({ email: event.target.value });
};

handleMealChange = event => {
  this.setState({ meal: event.target.value });
};

handleIsGoingChange = event => {
  this.setState({ isGoing: event.target.checked });
};

We can instead, use one handler that deals with all the fields’ needs? The React form doc in actually has a great example on one way to this:

handleChange = event => {
  const target = event.target;
  const value = target.type === 'checkbox' ? target.checked : target.value;
  const name = target.name;
  this.setState({
    [name]: value,
  });
};

Replace all the handle[Field]Change method with just that one. As long as the name of the fields matches their state properties, then the function above will be able to handle change for all fields including the checkbox.

Remember to change the function names we passed to the onChange prop:

<input
  <!-- highlight -->
  onChange="{this.handleChange}"
/>
<input
  <!-- highlight -->
  onChange="{this.handleChange}"
/>
<select
  <!-- highlight -->
  onChange="{this.handleChange}"
>
</select>
<input
  <!-- highlight -->
  onChange="{this.handleChange}"
/>

Improve Reuse with Render Prop

We could use one of React’s component reuse patterns, but we will make use of Render Props. Formik is one of my favorite React tools. It’s a component library that uses render props for abstracting most common for problems in React.

You can see how Jared Palmer used render props to build this library by watching the video below:

The video is pretty long. Basically, you need to install formik, then import it into the components where it will be used:

import { Formik } from 'formik';

Next, you should wrap the form in a Formik render prop:

<Formik onSubmit={this.handleSubmit}>
  {({ values, handleChange, handleSubmit }) => (
    <form className={styles.form} onSubmit={handleSubmit}>
      <div className={styles.formGroup}>
        <label htmlFor="name">Full Name</label>
        <input
          type="text"
          name="name"
          value={values.name}
          onChange={handleChange}
        />
      </div>
      <div className={styles.formGroup}>
        <label htmlFor="email">Email</label>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
      </div>
      <div className={styles.inlineGroup}>
        <div className={styles.formGroup}>
          <label htmlFor="meal">Meal Preference</label>
          <select name="meal" value={values.meal} onChange={handleChange}>
            <option value="1">Jollof Rice</option>
            <option value="2">Fried Rice</option>
          </select>
        </div>
        <div className={styles.formGroup}>
          <label htmlFor="meal">Is Going?</label>
          <input
            name="isGoing"
            type="checkbox"
            value={values.isGoing}
            onChange={handleChange}
          />
        </div>
      </div>
      <div className={styles.formGroup}>
        <button type="submit">Submit</button>
      </div>
    </form>
  )}
</Formik>

Few important things to take note of:

  1. The render prop receives an object where you can pick things like the current value of all fields, handleChange method (which we no longer need to write manually), and a handle submit method which is passed to the form element.
  2. The handleSubmit passed to the form needs to be defined as a value to onSubmit property of Formik.
  3. We have updated all fields to use values.[name] instead of this.state.[name] and to also use handleChange (from Formik) instead of this.handleChange.

The handleSubmit function should be updated to look like this:

handleSubmit = values => {
  alert(JSON.stringify(values, null, 2));
};

Feel free to delete handleChange since it’s no longer needed.

What should we take away from refactoring with render props? Notice how we are longer relying on the state. In fact, we can delete the state object now. Here are clear reasons why render props are preferred:

  1. Form states are ephemeral — they last for a very short time. Why bother with redux or even component state with such a temporary state. Formik is now handling the short-lived temporary state.
  2. We can reuse state and form logic without bothering about the details — just focus on creating and styling fields; leave the rest to Formik.

I have been using Formik, and it seemed like the best solution, until now. Until React Hooks happened.

Reusing Form Logic with React Hooks

The beauty of reusing form logic shines with React Hooks. This is why I am gradually moving from all the options above to this:

  1. If you have used hooks, you should be appreciative of the lovely API. It’s easier and more productive to build reusable logic.
  2. You can do anything a component can do with custom React Hooks. Especially when they need to be reused. Imagine having a hook called useForm

Custom React Hooks

Custom hooks allow us to build reusable logics. The word custom is as useless as use in hooks’ naming convention. This is because custom hooks are simply functions, but they can call other hooks like useState.

We are going to make a custom hook that behaves like Formik but offers a neater API. We want to name this hook useForm:

const useForm = ({ initialValues, onSubmit, validate }) => {
  ...
  return {
    values,
    touchedValues,
    errors,
    handleChange,
    handleSubmit,
    handleBlur
  };
};

Our new hook takes an object where we define the initial values, what happens on submission, and a validate function that we intend to run when a field is touched.

The function also returns an object where we can pick the current values, errors, touched, and the handlers. See this as the object Formik’s render prop hands you. We will create these returned values now.

Initial States

const useForm = ({ initialValues, onSubmit, validate }) => {
  // highlight line
  const [values, setValues] = React.useState(initialValues || {});
  const [touchedValues, setTouchedValues] = React.useState({});
  const [errors, setErrors] = React.useState({});

  return { ... };
};

We are creating the values, touchedValues, and errors pairs to hold and set some internal states. values and setValues will store and set the value of the form fields. touchedValues and setTouchedValues stores and sets the values of all touched fields. Touched means they have been edited or selected. Lastly, errors and setErrors obviously stores and sets the validation errors if any.

Event Handlers We have states, but something needs to trigger updates to these states. In the case of forms, states are always updated based on response to form events:

const useForm = ({ initialValues, onSubmit, validate }) => {
  //...
  const handleChange = event => {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;
    setValues({
      ...values,
      [name]: value
    });
  };

  const handleBlur = event => {
    const target = event.target;
    const name = target.name;
    setTouchedValues({
      ...touchedValues,
      [name]: true
    });
    const e = validate(values);
    setErrors({
      ...errors,
      ...e
    })
  };

  const handleSubmit = event => {
    event.preventDefault();
    const e = validate(values);
    setErrors({
      ...errors,
      ...e
    });
    onSubmit({ values, e });
  };
  return { ... };
};

Let’s see a detailed explanation of what each event is doing:

  1. The handleChange method is the same as what we encountered earlier. The only difference is that instead of using setState, we are using setValues.
  2. handleBlur listens to when the user touches a field and leaves that field. When that happens, we want to update the touchedValues. We also call the validate method to see if any of our fields have failed validation and we update the errors state too.
  3. Lastly, we use handleSubmit to call the onSubmit function we received from the hook passing it the values, touchedValues and errors. With this, you can also access these values before handling submission.

Using useForm

Hooks only work from inside a functional component. Therefore, we need to convert this component to a functional component:

function App() {
  return (
    <div className={styles.App}>
      <form className={styles.form} onSubmit={handleSubmit}>
        <h4 className={styles.formTitle}>
          Add Guest
          <hr />
        </h4>
        <div className={styles.formGroup}>
          <label htmlFor="name">Full Name</label>
          <input
            type="text"
            name="name"
            value={values.name}
            onChange={handleChange}
          />
        </div>
        <div className={styles.formGroup}>
          <label htmlFor="email">Email</label>
          <input
            type="email"
            name="email"
            value={values.email}
            onChange={handleChange}
          />
        </div>
        <div className={styles.inlineGroup}>
          <div className={styles.formGroup}>
            <label htmlFor="meal">Meal Preference</label>
            <select name="meal" value={values.meal} onChange={handleChange}>
              <option value="1">Jollof Rice</option>
              <option value="2">Fried Rice</option>
            </select>
          </div>
          <div className={styles.formGroup}>
            <label htmlFor="meal">Is Going?</label>
            <input
              name="isGoing"
              type="checkbox"
              value={values.isGoing}
              onChange={handleChange}
            />
          </div>
        </div>
        <div className={styles.formGroup}>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  );
}

Then we need to get the values and event handlers from the hook. Just before the return line, call the useForm function:

function App() {
  const {
    values,
    handleChange,
    handleSubmit,
  } = useForm({
    initialValues: {
      name: "",
      email: "",
      meal: "",
      isGoing: false
    },
    onSubmit(values, errors) {
      alert(JSON.stringify({ values, errors }, null, 2));
    },
    validate(values) {
      const errors = {};
      if (values.name === "") {
        errors.name = "Please enter a name";
      }
      return errors;
    }
  });

  return (...)
}

Remember that useForm returns an object which has the values, event handlers, errors (if any), etc. We are cherry-picking the properties we need from that object for the form that comes after.

The onSubmit method receives the values and errors so you can either post to a server here and handle errors if the errors object is not empty. The validate method keeps track of errors in the errors object and returns the object. The submit handler receives these errors.

Moving Forward

You can see how neat and simple it is to use useForm as a hook API to build reusable form logic. We can keep extending to handle everything for whatever use-case we might have for the form including state tracking, event handling, validation, etc. Feel free to hit me up via email, comment or twitter if you got an idea on how to make this even better. No idea would be too simple!


Christian Nwamba

Written by Christian Nwamba.
Follow on Twitter

© 2019, Codebeast.dev