How to Create a Custom Email Signup Form for ConvertKit on GatsbyJS.
Tags: javascript, gatsby, email
Published: 2021-05-27 20:01:40

---
title: How to Create a Custom Email Signup Form for ConvertKit on GatsbyJS.
published: true
date: 2021-05-01 00:00:00 UTC
tags: Javascript, Gatsby, Email
canonical_url: https://conermurphy.com/blog/custom-email-signup-form-conertkit-gatsbyjs/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d3j10z2fu0i3yxnov2j5.png
---

Welcome back to my blog.

We all know the benefits that building a following online can bring. And, one of the most powerful tools for someone looking to build a following online is an email newsletter.

But, just having a newsletter isn't enough, we also need a way for people to sign up to it with minimal effort.

That's why in this post, I'm going to show you how I built a custom email newsletter signup form for ConvertKit on my GatsbyJS website. Let's do this.

There are 4 parts to building a custom subscriber form, these are:

1. The signup component users will interact with.
2. A custom hook to handle the form changes.
3. A custom hook to handle the submitting of the form.
4. A serverless function to actually submit the request.

Let's cover each one individually and see how the data flows between them.

## The Signup Component

Because we are just building an email signup form the only inputs we need is a text input for the email and a submit button.

Here's a look at the code:

```
export const EmailSignup = () => {
  const { values, updateValue } = useForm({
    email: ''
  });
  const { message, loading, error, submitEmail } = useEmail({ values });
  const { email } = values;

  return (
    <>
      <FormGridContainer onSubmit={submitEmail}>
        <fieldset disabled={loading}>
          <label htmlFor="email">
            Email:
            <input
              type="email"
              name="email"
              id={`email-${Math.random().toString(36).substring(2, 15)}`}
              className="emailInput"
              onChange={updateValue}
              value={email}
            />
          </label>
        </fieldset>
        <button className="signupButton" type="submit" disabled={loading}>
          {loading ? 'Subscribing...' : ' Subscribe'}
        </button>
      </FormGridContainer>
      {message ? <OutcomeMessageContainer error={error} message={message} /> : ''}
    </>
  );
};
```

In the first part, we handle passing the data to and from the 2 helper functions that we will create: `useForm` and `useEmail`.

Then for the rest of the component, we handle displaying the data back to the user in the form and creating elements for them to interact with.

The only other part to note is at the bottom of the code. The component `OutcomeMessageContainer` is a styled-component that looks like this:

```
const OutcomeMessageContainer = ({ error, message }) => (
  <MessageContainer>
    {error ? <FaTimes data-error /> : <FaCheck />}
    <p>{message}</p>
  </MessageContainer>
);
```

As you can see we pass in 2 props, the error if there is one and the message returned back from the serverless function. we then display these to the user.

Now, let's look at the first helper function: `useForm`.

## useForm

`useForm` is a small helper function to aid in the recording and displaying of information in the form.

It expands to include new values if required so all we need to do is pass in new defaults.

This is important because we want an easy way to access the data to pass to the next helper function `useEmail`.

Here's the code for `useForm`.

```
import { useState } from "react";

export default function useForm(defaults) {
  const [values, setValues] = useState(defaults);

  function updateValue(e) {
    // Get value from the changed field using the event.
    const { value } = e.target;

    // Set the value by spreading in the existing values and chaging the key to the new value or adding it if not previously present.
    setValues({
      ...values,
      [e.target.name]: value,
    });
  }

  return { values, updateValue };
}
```

Essentially, it boils down to a `useState` hook and a function to set the state.

The state it sets is an object containing the current values and any added ones.

For example, in our case the object set to the state would look like:

```
{
  email: "example@example.com";
}
```

If we then look back at our original component where we consume this hook you can see how we use it:

```
const { values, updateValue } = useForm({
  email: "",
});

const { email } = values;
```

First, we destructure out the `values` and the `updateValue` function that we returned. Then we destructure out the individual values beneath that.

When calling the hook we have to provide some default values for the hook to set to state.

We do this because otherwise when we access the `email` value on page load, it won't exist causing an error. To prevent this we create all the required state on load with a default value.

We then update this state as required.

Then on the input element in the form, we pass the `updateValue` function as the `onChange` handler like so:

```
<input
  type="email"
  name="email"
  id={`email-${Math.random().toString(36).substring(2, 15)}`}
  className="emailInput"
  onChange={updateValue}
  value={email}
/>
```

How does it know what index to update you may be asking?

Well, looking back at our `useForm` code, in the `updateValue` function:

```
function updateValue(e) {
  // Get value from the changed field using the event.
  const { value } = e.target;

  // Set the value by spreading in the existing values and chaging the key to the new value or adding it if not previously present.
  setValues({
    ...values,
    [e.target.name]: value,
  });
}
```

Here you can see that we destructure out the `value` we want to set to state from the event with `e.target`. Then when we set the state we get the `name` of the input from `e.target` again to be the key.

Looking at the above code, the name of the input element is `email`. This will update the state with the key `email` with the value from the target element.

To summarise:

- We pass in a default state for `email` as an empty string.
- Then use an `onChange` handler to update this state at a later date as the user begins to type in it.

## useEmail

Let's take a look at the next helper function.

Here's the entire code which we will break down in a second:

```
import { useState } from "react";

export default function useEmail({ values }) {
  // Setting state to be returned depending on the outcome of the submission.
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState("");
  const [error, setError] = useState();

  // destructuring out the values from values passed to this form.
  const { email } = values;

  const serverlessBase = process.env.GATSBY_SERVERLESS_BASE;

  async function submitEmail(e) {
    // Prevent default function of the form submit and set state to defaults for each new submit.
    e.preventDefault();
    setLoading(true);
    setError(null);
    setMessage(null);

    // gathering data to be submitted to the serverless function
    const body = {
      email,
    };

    // Checking there was an email entered.
    if (!email.length) {
      setLoading(false);
      setError(true);
      setMessage("Oops! There was no email entered");
      return;
    }

    // Send the data to the serverless function on submit.
    const res = await fetch(`${serverlessBase}/emailSignup`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    // Waiting for the output of the serverless function and storing into the serverlessBaseoutput var.
    const output = JSON.parse(await res.text());

    // check if successful or if was an error
    if (res.status >= 400 && res.status < 600) {
      // Oh no there was an error! Set to state to show user
      setLoading(false);
      setError(true);
      setMessage(output.message);
    } else {
      // everyting worked successfully.
      setLoading(false);
      setMessage(output.message);
    }
  }

  return {
    error,
    loading,
    message,
    submitEmail,
  };
}
```

It's a bit of a long one but it breaks down into logical chunks:

1. We create some state with default values, destructure values from the props and get the serverless base from our `.env`

```
export default function useEmail({ values }) {
  // Setting state to be returned depending on the outcome of the submission.
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState("");
  const [error, setError] = useState();

  // destructuring out the values from values passed to this form.
  const { email } = values;

  const serverlessBase = process.env.GATSBY_SERVERLESS_BASE;

  // ... Rest of function
}
```

At this point the function hasn't been called, so we create the state accordingly:

```
loading -> false -> Not waiting on anything as not called
message -> "" -> No info returned so blank by default
error -> <EMPTY> -> No error has been generated as not called.
```

Then we destructure out the value we're interested in from the props `email`. This will be passed to the body of the submission request in a moment.

Then we get the serverless base from the `.env` file.

[Learn more about Serverless Netlify Functions.](https://docs.netlify.com/functions/overview/)

### Defining the submitEmail function

Now, we're going to look at our `submitEmail` function, this is what will be called when the form is submitted. (We'll come back to this in a moment.)

```
async function submitEmail(e) {
  // Prevent default function of the form submit and set state to defaults for each new submit.
  e.preventDefault();
  setLoading(true);
  setError(null);
  setMessage(null);

  // gathering data to be submitted to the serverless function
  const body = {
    email,
  };

  // Checking there was an email entered.
  if (!email.length) {
    setLoading(false);
    setError(true);
    setMessage("Oops! There was no email entered");
    return;
  }

  // Send the data to the serverless function on submit.
  const res = await fetch(`${serverlessBase}/emailSignup`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  // Waiting for the output of the serverless function and storing into the output var.
  const output = JSON.parse(await res.text());

  // check if successful or if was an error
  if (res.status >= 400 && res.status < 600) {
    // Oh no there was an error! Set to state to show user
    setLoading(false);
    setError(true);
    setMessage(output.message);
  } else {
    // everyting worked successfully.
    setLoading(false);
    setMessage(output.message);
  }
}
```

Once again, let's break this down into steps.

1. First we prevent the default behaviour of the form and update the state values we defined earlier on to show we're waiting on the request.

```
// Prevent default function of the form submit and set state to defaults for each new submit.
e.preventDefault();
setLoading(true);
setError(null);
setMessage(null);
```

1. We create the body of the request we're going to submit by using the values from earlier.

```
// gathering data to be submitted to the serverless function
const body = {
  email,
};
```

1. Before submitting the form, we check if the email length is greater than 0 or is truthy. If it isn't then we update the state to be an error, pass a custom error message and return the function.

```
// Checking there was an email entered.
if (!email.length) {
  setLoading(false);
  setError(true);
  setMessage("Oops! There was no email entered");
  return;
}
```

1. If the email value is truthy, then we progress with the submission and do a `POST` request to the serverless function with the `body` we created.

```
// Send the data to the serverless function on submit.
const res = await fetch(`${serverlessBase}/emailSignup`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(body),
});
```

1. We await the reply from the serverless function, once received we convert it to text and parse it with `JSON.parse`.

```
// Waiting for the output of the serverless function and storing into the output var.
const output = JSON.parse(await res.text());
```

1. Then we get to the final part of the submit function. We check if the request was successful or not and set the state accordingly.

```
// check if successful or if was an error
if (res.status >= 400 && res.status < 600) {
  // Oh no there was an error! Set to state to show user
  setLoading(false);
  setError(true);
  setMessage(output.message);
} else {
  // everyting worked successfully.
  setLoading(false);
  setMessage(output.message);
}
```

### Returning data

After we have processed the request, we return the below info back out of the helper function:

```
return {
  error,
  loading,
  message,
  submitEmail,
};
```

This gives us access to all the state we defined as well the `submitEmail` function we defined.

### Using it in the Component

Back in the main component, we destructure out the values from the `useEmail` function like so:

```
const { message, loading, error, submitEmail } = useEmail({ values });
```

We consume the destructured values in the following places:

1. `onSubmit` function for the form

```
<FormGridContainer onSubmit={submitEmail}>
```

1. Disabling the submit button if loading is true and changing the text within it.

```
<button className="signupButton" type="submit" disabled={loading}>
  {loading ? "Subscribing..." : " Subscribe"}
</button>
```

1. We use the display component from earlier to show the message to the user and whether there has been an error or not.

```
{
  message ? <OutcomeMessageContainer error={error} message={message} /> : "";
}
```

Now we just need to look at the serverless function.

## The Serverless Function

Let's now take a look at how we're going to use the information from `useEmail` in our serverless function to submit the request to ConvertKit.

But, before you can do this you'll need to get yourself an API key and create a form on ConvertKit. If you want to [read more about their API, click here.](https://developers.convertkit.com/)

The API endpoint we will be making use of is:

`https://api.convertkit.com/v3/forms/`

Once you have your form id from the form you created and your API key we can get started.

Here is the full code:

```
require("isomorphic-fetch");

exports.handler = async (event) => {
  const body = JSON.parse(event.body);

  // Checking we have data from the email input
  const requiredFields = ["email"];

  for (const field of requiredFields) {
    if (!body[field]) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: `Oops! You are missing the ${field} field, please fill it in and retry.`,
        }),
      };
    }
  }

  // Setting vars for posting to API
  const endpoint = "https://api.convertkit.com/v3/forms/";
  const APIKey = process.env.CONVERTKIT_PUBLIC_KEY;
  const formID = process.env.CONVERTKIT_SIGNUP_FORM;

  // posting to the Convertkit API
  await fetch(`${endpoint}${formID}/subscribe`, {
    method: "post",
    body: JSON.stringify({
      email: body.email,
      api_key: APIKey,
    }),
    headers: {
      "Content-Type": "application/json",
      charset: "utf-8",
    },
  });
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Success! Thank you for subscribing! 😃" }),
  };
};
```

Let's walk through this again as we did with the other functions:

1. Parse the JSON body that was sent from `useEmail`.

```
const body = JSON.parse(event.body);
```

1. Check we have filled the required fields, if not return an error saying they were missed.

```
// Checking we have data from the email input
const requiredFields = ["email"];

for (const field of requiredFields) {
  if (!body[field]) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        message: `Oops! You are missing the ${field} field, please fill it in and retry.`,
      }),
    };
  }
}
```

1. Get our variables from `.env` and then submit a `POST` request to ConvertKit.

```
// Setting vars for posting to API
const endpoint = process.env.CONVERTKIT_ENDPOINT;
const APIKey = process.env.CONVERTKIT_PUBLIC_KEY;
const formID = process.env.CONVERTKIT_SIGNUP_FORM;

// posting to the Convertkit API
await fetch(`${endpoint}${formID}/subscribe`, {
  method: "post",
  body: JSON.stringify({
    email: body.email,
    api_key: APIKey,
  }),
  headers: {
    "Content-Type": "application/json",
    charset: "utf-8",
  },
});
```

1. Process the return

```
return {
  statusCode: 200,
  body: JSON.stringify({ message: "Success! Thank you for subscribing! 😃" }),
};
```

This is a smaller function because we did a lot of heavy lifting in the `useEmail` function.

As long as the required fields are populated we shouldn't have any issues with the request going through.

## The Flow

To round up this post and tie together all of the steps we've gone through, let's look at the flow of data:

```
Email Form Component
👇
UseForm -> For storing form info
👇
Email Form Component
👇
useEmail -> onSubmit send the info to the serverless function
👇
Serverless Function -> Submit to ConverKit
👇
Email Form Component -> Display the success message
```

There's a fair amount going on between several files but the flow isn't too complicated. Obviously, if stuff goes wrong then the flow can be short-circuited.

The 2 main places for a short-circuit to happen would be `useEmail` and the serverless function.

## Summing Up

I have been running a setup very similar to this on my website now for a few months and haven't had any issues. I like having all the functions separated into their own files as I do think it improves the readability.

The one thing we could add to improve this setup would be a Honeypot to capture any robots trying to fill in the form. But, I plan on covering this in a separate post where I can go more in-depth.

I didn't include any styling in this post for brevity, but if you're interested you can [see it all on my GitHub here.](https://github.com/conermurphy/conermurphy.com)

What do you think of this setup? Let me know over on [Twitter.](https://twitter.com/MrConerMurphy)

I hope you found this post helpful. If you did please consider sharing it with others. If you would like to see more content like this please consider following me on [Twitter.](https://twitter.com/MrConerMurphy)