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:
- Create new contact.js page in Gatsby
- Create form fields and hook them into React Hook Form
- Write the onSubmit function
- 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.