Gatsby email contact forms with Twilio Functions and React

July 01, 2020
Written by
Reviewed by
Diane Phan
Twilion

Gatsby email contact forms with Twilio Functions and React

Today I’ll teach you how to send an email from a Gatsby website using Twilio Sendgrid, Twilio Serverless Functions and React. But first, what is Gatsby and why should you care?

Gatsby 101

If you spend time in JavaScript land, you might have heard of the JAMstack -- JavaScript, APIs, and Markup.

Traditional web apps dynamically generate HTML from templates when the server receives a request. JAMstack websites use static site generators to turn templates into HTML when the site is deployed, instead of when a request comes in. 

Since plain ol’ HTML can be much faster to serve, JAMstack can give you a huge performance boost. Especially if you’re using a content delivery network. Although, there are limits to the amount of UI complexity static sites can support.

Gatsby bridges the gap between JAMstack static sites and traditional web apps. You write React components that compile to HTML plus a little bit of JavaScript to support user interactivity. Gatsby is essentially a hybrid where you have the best of both worlds.

For more detail, check out this thorough blog post about how Gatsby and other React apps differ.

Gatsby is also fully open source and run by a foundation. And it’s got a whole ecosystem with plugins, themes, and starters that empower you to build on the work of others.

Your first Gatsby site

Before you do anything else you’ll need to set up your Gatsby development environment.

A starter is an example of an entire Gatsby site you can clone and build on top of. From the terminal, run the following command to create a new project using the default starter:

gatsby new my-default-starter https://github.com/gatsbyjs/gatsby-starter-default

Open the my-default-starter folder in your favorite editor. Run the following command to get started developing:

  cd my-default-starter
  gatsby develop

Amongst the output you should see something like:

You can now view gatsby-starter-default in the browser.

Go to that URL in a browser and see the Gatsby default starter running on your local host. 🎉

Screenshot of Gatsby default starter app running on localhost. The text says "Hi people. Welcome to your new Gatsby site. Now go build something great."

Add an email contact form to Gatsby

Now we’re going to modify the default starter by adding a form. Gatsby project structure isn’t too important for our purposes today, but if you want to learn more check out the Gatsby docs.

In the src/components folder, create a new file called contactForm.js. Add the following code to it:

import React from "react"

class ContactForm extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     buttonDisabled: true,
     message: { fromEmail: "", subject: "", body: "" },
     submitting: false,
     error: null,
   }
 }

 onChange = event => {
   const name = event.target.getAttribute("name")
   this.setState({
     message: { ...this.state.message, [name]: event.target.value },
   })
 }
 render() {
   return (
     <>
       <div>{this.state.error}</div>
       <form
         style={{
           display: `flex`,
           flexDirection: `column`,
           maxWidth: `500px`,
         }}
         method="post"
       >
         <label htmlFor="fromEmail">Your email address:</label>
         <input
           type="email"
           name="fromEmail"
           id="fromEmail"
           value={this.state.message.fromEmail}
           onChange={this.onChange}
         ></input>
         <label htmlFor="subject">Subject:</label>
         <input
           type="text"
           name="subject"
           id="subject"
           value={this.state.message.subject}
           onChange={this.onChange}
         />
         <label htmlFor="body">Message:</label>
         <textarea
           style={{
             height: `150px`,
           }}
           name="body"
           id="body"
           value={this.state.message.body}
           onChange={this.onChange}
         />
         <button
           style={{
             marginTop: `7px`,
           }}
           type="submit"
           disabled={this.state.submitting}
           onClick={this.onClick}
         >
           Send message
         </button>
       </form>
     </>
   )
 }
}

export default ContactForm

The form asks for the sender’s email address, and for the text that goes in the body of the message. The contactForm component manages this data as React state so that it’s easy to submit and clear the data. The form also has some state to disable the submit button while the form is being submitted, and to show error text if any exists.

Before we can see our work, we’ll need to add contactForm into our component tree. Open up pages/index.js and add the following lines:

import React from "react"
import { Link } from "gatsby"

import Layout from "../components/layout"
import Image from "../components/image"
import SEO from "../components/seo"
import ContactForm from "../components/contactForm"

const IndexPage = () => (
 <Layout>
   <SEO title="Home" />
   <h1>Hi people</h1>
   <p>Welcome to your new Gatsby site.</p>
   <p>Now go build something great.</p>
   <ContactForm></ContactForm>
   <div style={{ maxWidth: `300px`, marginBottom: `1.45rem` }}>
     <Image />
   </div>
   <Link to="/page-2/">Go to page 2</Link>
 </Layout>
)

export default IndexPage

Save index.js in your editor. As soon as you do so, the browser reloads automatically and our form shows up. Neat!

Screenshot of Gatsby default starter running on localhost, with the addition of an email contact form. The form has fields for "Your email address", "subject", and "message" in addition to a submit button.

Configure SendGrid

If you don’t have a SendGrid account now’s the time to sign up for one. Follow these steps to verify a sender identity. After you’ve done that create your SendGrid API key with “Full Access” permissions. Give it a descriptive name, like “Gatsby contact form.”

screenshot of the SendGrid page for creating an API key. The key name is "Gatsby contact form" and the permissions are "Full Access."

Copy the API key. You’re going to need it in a minute.

Add a serverless function

Our form doesn’t do anything yet, so let’s fix that. To integrate SendGrid into the site, we’ll need some sort of back end. Storing account credentials on the front end is a “DON”T”, as that exposes them to potential attackers. Luckily for us, the "A" in JAMstack stands for APIs, which includes serverless functions. We don’t need to spin up an entire server just to send a message.

Go to the Twilio Functions dashboard. Add @sendgrid/mail to the “Dependencies” section. In the “Environment Variables” section, add a new environment variable called SENDGRID_API_KEY for the SendGrid API key you just created.

Click “Manage“ and then click the “+” button to create a new function from a blank template. Your function will need a name and a path. Let’s go with “Send Email” and “send-email” respectively.

Uncheck the box that says “Check for valid Twilio signature” since we’ll be making a request from a non-Twilio domain. Copy the following code into the function body.

Replace the to and from values with your email address. We can’t send email directly from an unverified address, because that makes spoofing too easy. Instead, we’ll dump the email address from the contact form in the body of the email.

const sgMail = require("@sendgrid/mail")

exports.handler = async function (context, event, callback) {
 sgMail.setApiKey(context.SENDGRID_API_KEY)

 const response = new Twilio.Response()
 response.setHeaders({
   "Access-Control-Allow-Origin": "http://localhost:8000",
   "Content-Type": "application/json",
 })

 const text = `from: ${event.fromEmail}, body: ${event.body}`

 const message = {
   to: "", // replace this with your email address
   from: "", // replace this with your email address
   subject: event.subject,
   text,
 }

 try {
   await sgMail.send(message)
   response.setBody({})
   response.setStatusCode(200)
 } catch (error) {
   response.setBody({ error: error.message })
   response.setStatusCode(error.code)
 }

 callback(null, response)
}

At a high level this code is passing along the sender’s email, subject line, and text message along to our SendGrid client, which then sends the email to the specified recipient. Since we will be making a request from localhost:8000 to a different domain, we need to set CORS headers in our function response or the request will fail. The Twilio response object gives us granular control over headers and status codes. Which also enables us to pass any errors through to the client so that the user knows what’s going on when things fail.

If you were going to deploy your Gatsby site to staging or production, you’d need to refactor this code a little. A CORS header can only contain one domain. You’d need some server-side logic to dynamically construct the header based on the list of domains you want to allow.

Hit the “Save” button so that your function deploys itself. Copy the path of your function because we’ll need it in a second.

Going back to your editor, let’s modify the ContactForm to actually make a request to the serverless function. Copy the following code into contactForm.js and replace the functionURL with your own.

import React from "react"

const functionURL = "" // replace this with your function URL

class ContactForm extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     buttonDisabled: true,
     message: { fromEmail: "", subject: "", body: "" },
     submitting: false,
     error: null,
   }
 }

 onClick = async event => {
   event.preventDefault()
   this.setState({ submitting: true })
   const { fromEmail, subject, body } = this.state.message

   const response = await fetch(functionURL, {
     method: "post",
     headers: {
       "Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
     },
     body: new URLSearchParams({ fromEmail, subject, body }).toString(),
   })
   if (response.status === 200) {
     this.setState({
       error: null,
       submitting: false,
       message: {
         fromEmail: "",
         subject: "",
         body: "",
       },
     })
   } else {
     const json = await response.json()
     this.setState({
       error: json.error,
       submitting: false,
     })
   }
 }

 onChange = event => {
   const name = event.target.getAttribute("name")
   this.setState({
     message: { ...this.state.message, [name]: event.target.value },
   })
 }
 render() {
   return (
     <>
       <div>{this.state.error}</div>
       <form
         style={{
           display: `flex`,
           flexDirection: `column`,
           maxWidth: `500px`,
         }}
         method="post"
         action={functionURL}
       >
         <label htmlFor="fromEmail">Your email address:</label>
         <input
           type="email"
           name="fromEmail"
           id="fromEmail"
           value={this.state.message.fromEmail}
           onChange={this.onChange}
         ></input>
         <label htmlFor="subject">Subject:</label>
         <input
           type="text"
           name="subject"
           id="subject"
           value={this.state.message.subject}
           onChange={this.onChange}
         />
         <label htmlFor="body">Message:</label>
         <textarea
           style={{
             height: `125px`,
           }}
           name="body"
           id="body"
           value={this.state.message.body}
           onChange={this.onChange}
         />
         <button
           style={{
             marginTop: `7px`,
           }}
           type="submit"
           disabled={this.state.submitting}
           onClick={this.onClick}
         >
           Send message
         </button>
       </form>
     </>
   )
 }
}

export default ContactForm

We added a click handler function to handle the form submission. Our click handler makes a fetch request to the serverless function. If all is well we reset the form state to blank and the message is sent. If there’s an error, we pop that into the component’s state so we can show the user what went wrong.

Try the form out and send yourself any message you’d like.

Screenshot of Gatsby default starter running on localhost, with an email contact form. The form is filled out with "Your email address" as "jay.gatsby@great.org", subject like "an extraordinary gift for hope", message "And so with the sunshine and the great bursts of leaves growing on the trees, just as things grow in fast movies, I had that familiar conviction that life was beginning over again with the summer."

Conclusion: Make an email contact form with Gatsby and React

Today you’ve learned a little about Gatsby and how it fits into the JAMStack world, as well as how to integrate Gatsby and SendGrid with serverless functions. But there’s always more to learn. Gatsby has fantastic documentation, so if you want to dive in deeper I’d encourage you to start there.

If you have built something cool with Gatsby, I’d love to hear about it. Let me know on Twitter or in the comments below.