This is part 2, of the 2 part series on creating a contact form for your Gatsby powered blog. In part 1, I showed you how to set up the backend on AWS. In this post I'lll show you the front-end code for validating and submitting form data. You can see a finished version of all this on the contact page.

There are 4 steps to get our contact page built and working:

  1. Create new contact.js page in Gatsby
  2. Create form fields and hook them into React Hook Form
  3. Write the onSubmit function
  4. Handle form validation, errors, and success messaging.

Contact page in Gatsby

This is the easier part. Just create a new file in your Gatsby project and export a simple header for now.

src/pages/contact.js

import React from 'react';

export default () => {
  return <h1>Contact form</h1>;
};

React Hook Form

React Hook Form (RHF) is a new hook based library that brings hooks to React forms. I've used Formik, and Redux Form extensively in the past but I have to say once I started using React Hook Form, I can't see myself going back.

To hook your form elements into RHF, all you have to do is ref them with its register hook.

// 1
import useForm from 'react-hook-form';

// 2
const { register } = useForm();

// 3
<input ref={register} />;

That's all. There's a ton more you can do but that's really the only thing you have to do to get the ball rolling. Below is the full version of contact.js with the 3 new fields for our email Lambda: Name, Email, and Question.

I've made sure to use accessible labels with matching ids, as well as adding some placeholder text.

import React, { useState } from 'react';
import useForm from 'react-hook-form';

export default () => {
  const { register } = useForm();

  const showForm = (
    <form method="post">
      <label htmlFor="name">
        <h3>Name</h3>
        <input
          type="text"
          name="name"
          id="name"
          placeholder="Enter your name"
          ref={register}
        />
      </label>

      <label htmlFor="email">
        <h3>Email</h3>
        <input
          type="email"
          name="email"
          id="email"
          placeholder="your@email.address"
          ref={register}
        />
      </label>

      <label htmlFor="question">
        <h3>Question</h3>
        <textarea
          ref={register}
          name="question"
          id="question"
          rows="3"
          placeholder="Say something"
        />
      </label>

      <button type="submit">Send</button>
    </form>
  );

  return (
    <div>
      <h1>Contact form</h1>
      {showForm}
    </div>
  );
};

Write the onSubmit function

Now let's get the form data out of our form. React Hook Form gives you a callback hook called handleSubmit. You call it, and pass it your own callback function for handling submission logic, which will receive the form data as an object.

We turn on CORS mode in the fetch request, and set the content type to application/json, passing the stringified data object as the body of the request to API Gateway.

To illustrate this I'm going to use a dummy API endpoint, but you can replace this with your own API Gateway URL from part 1 of this tutorial. Note that AWS returns a null response in case a of a successful post. We wrap the fetch in a try/catch block so that we can capture any potential server errors.

const { handleSubmit } = useForm();
const FAKE_GATEWAY_URL = 'https://jsonplaceholder.typicode.com/posts';

const onSubmit = data => {
  try {
    fetch(FAKE_GATEWAY_URL, {
      method: 'POST',
      mode: 'cors',
      cache: 'no-cache',
      body: JSON.stringify(data),
      headers: {
        'Content-type': 'application/json; charset=UTF-8',
      },
    });
  } catch (error) {
    // handle server errors
  }
};

<form onSubmit={handleSubmit(onSubmit)} method="post">
  <!-- form fields -->
</form>

Handle form validation, errors, and success messaging

Now we want to do lots of housekeeping. We want to perform validation for our fields, and display error messaging. We want to disable the form fields while the form is being submitted. We also want to reset the form and show the user a confirmation message once the form data has been successfully submitted.

Input validation

The register callback takes an optional configuration object. Passing required as a key, activates the built in validation of RHF. All you need to pass it is a string to show if the field is left blank. For example here's how you would make the email field required.

<input
  type="email"
  name="email"
  ref={register({
    required: 'Email is required',
  })}
/>

For most sophisticated forms, you'll want a bit more control over the validation logic, and luckily RHF supports yup for validation, as well as its own built in validation.

Error messaging

If validation fails, RFH will update the errors object with the error messages. We will inspect that object and if we find keys matching our form fields we will display the error strings back to the user.

const { errors } = useForm();

{
  errors.email && errors.email.message;
}

Disabling form fields while submitting

The formState hook itself contains a set of hooks, one of which is the isSubmitting boolean. We can use this to avoid strange UX bugs, by disabling our fields while the form is in the process of being submitted.

const { formState: {
  isSubmitting
}} = useForm();

<input disabled={isSubmitting} />
<button disabled={isSubmitting} type="submit">
  Send
</button>

Resetting the form

By using the reset hook we can reset the form fields after the data has successfully submitted. To get our timing right, we'll make our onSubmit function async and await the completion of our fetch call, before calling the reset hook, which will clear out all user inputs.

const { reset } = useForm();

const onSubmit = async data => {
  try {
    await fetch(AWS_GATEWAY_URL, {
      // fetch options
    });
    reset();
  } catch (error) {
    // handle server errors
  }
};

Handling server errors

In our catch block, we want to capture any exceptions and store that state somewhere so we can notify the user. The setError hook is exactly what we need. It can be used to set both form errors, and arbitrary errors programatically. I'm using a submitError type here to store an error. We will then test and show it in our render using a ternary.

const { setError } = useForm();

const onSubmit = async data => {
  try {
    // fetch
  } catch (error) {
    setError('submit', 'submitError', `Doh! ${error.message}`);
  }
};

return (
  {errors && errors.submit && errors.submit.message}
)

Showing a confirmation message

Finally, we will show the user confirmation messaging once we know the form has been successfully posted, to complete the feedback loop. To achieve this, we'll useState to store and update a submitted boolean. This can happen at the same place we are already calling the reset function. We will show either the contact form, or a confirmation message with a button to show the form again.

import { useState } from 'react';

const [submitted, setSubmitted] = useState(false);

const onSubmit = async data => {
  try {
    // await fetch;
    // reset();
    setSubmitted(true);
  } // catch errors
};

const showThankYou = (
  <div>Thank you!
    <button onClick={() => setSubmitted(false)}>go back</button>
  </div>
);

const showForm = <form />;

return (
  {submitted ? showThankYou : showForm}
)

That's all! Here's a completed working version on codesandbox for your reference.