Kick-Start Your Newsletter: Mailchimp React Form
Tags: react, tutorial, webdev
Published: 2020-08-15 16:49:09

---
title: Kick-Start Your Newsletter: Mailchimp React Form
published: true
description: Learn how to create a custom newsletter sign up form in React with JSONP and serverless functions.
tags: React, Tutorial,  WebDevelopment
cover_image: https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597480636/kick-start-your-newsletter-mailchimp-custom-form-with-react/Group_1_figtnq.png
canonical_url: https://jfelix.info/blog/kick-start-your-newsletter-mailchimp-custom-form-with-react
---

Recently, I had to create a subscription form for my [blog/portfolio website](https://jfelix.info/). I decided to use Mailchimp because of its popularity and how highly efficient it is at handling subscribers. They have many form templates for getting started quickly. However, these templates handle the requests directly on the form's HTML instead of through a Front-End library like React. This is great for a simple website that does not require different states or interactions. Nonetheless, For my site, I wanted to use React to create a custom experience for my users. Through various attempts, I was able to find a testable and efficient way to handle these forms to React without sacrificing interactivity. 

In this article, I will explain how we could implement a custom Mailchimp form with React using JSONP or serverless function. 

## Serverless Functions or JSONP

You may be asking what should you choose between JSONP and serverless. My recommendation would be to go with serverless since it is more flexible, secure, and simpler to test. However, it requires you to set up a serverless provider like [Amazon AWS](https://aws.amazon.com/lambda/), [Vercel](https://vercel.com/), [Netlify](https://www.netlify.com/products/functions/), etc which is not always possible in certain situations. On the other hand, JSONP is really easy to get started, and we only need some dependencies along. 

In this post, I'm going to implement it first with JSONP, and then with serverless.   

## Getting Your Mailchimp's Generated Form URL

Before we can start, we need to get the URL for sending a subscription request. Since we are going to be doing both a JSONP and serverless, we will have to get the URL of the solution we need.

## Getting JSONP URL

To get the URL, head to **Signup Forms** under **Audience** in your admin account dashboard. \*\*\*\*

![Select Signup forms](https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597480828/kick-start-your-newsletter-mailchimp-custom-form-with-react/mailchimp__1_mqwnnm.png)

Once there, select **Embedded Forms**.

![Select embedded forms](https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597480864/kick-start-your-newsletter-mailchimp-custom-form-with-react/mailchimp__4_kdu69c.png)

Under **Copy/paste onto your site**, look for the URL inside the action attribute in the form. This is the URL we will be using. 

![Arrow pointing to action attribute](https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597480888/kick-start-your-newsletter-mailchimp-custom-form-with-react/mailchimp__5_xl4ar5.png)

That's it, nice job! Now, you can start creating your JSONP hook. 

## Creating a Hook For Subscribing Through JSONP

The reason why we are going to use JSON with Padding (JSONP) instead of fetch/Axios is that if we try to do a POST request to Mailchimp servers we will get a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) error. This happens because by default the fetch/Axios API doesn't allow creating requests to different origins unless the response from the other domain includes the right CORS headers. Unfortunately, Mailchimp's will never send the right CORS header for security reasons. JSONP is one interesting technique that allows us to make a GET request to servers of different origins. It basically allows us to bypass the CORS policy.  

The first step for creating our hook is to install the [jsonp](https://www.npmjs.com/package/jsonp) package:

```bash
npm install jsonp
// or
yarn add jsonp
```

First, we declared a `Status` object which contains all of the possible states of our request, so we can determine the state in our form in a predictable way.

```jsx
export const Status = {
	idle: 'IDLE',
	loading: 'LOADING',
	success: 'SUCCESS',	
	error: 'ERROR'
}
```

Second, we declare a `toQueryString` function which converts an object to a query string to be added as URL parameters. 

```jsx
/*

	Converts {foo: 'bar', key: 'bar2'} to 'foo=bar&key=bar2'

*/

function toQueryString(params) {
  return Object.keys(params)
    .map((key) => key + "=" + params[key])
    .join("&");
}
```

Third, inside our hook, we declare two React state variables, one for the error output and the other for the value received. 

```jsx
const [status, setStatus] = React.useState(Status.idle);
const [error, setError] = React.useState(null);
const [value, setValue] = React.useState(null);
```

Finally, we have a subscription method that will receive the data we need for the user to subscribe. This subscribe method first converts all our data into a query string and appends it to a new URL that replaces the `post` with `post-json` resource. Then we proceed to do the request while setting the status and error as needed. 

```jsx
const subscribe = React.useCallback((data) => {
    const params = toQueryString(data);
    const ajaxURL = url.replace("/post?", "/post-json?");
    const newUrl = ajaxURL + "&" + params;

    setError(null);
    setStatus(Status.loading);

    jsonp(newUrl, { param: "c" }, (err, data) => {
      if (err) {
        setStatus(Status.error);
        setError(err);
      } else if (data.result !== "success") {
        setStatus(Status.error);
        setError(data.msg);
      } else {
        setStatus(Status.success);
        setValue(data.msg);
      }
    });
  }, []);
```

Putting all these together: 

```jsx
export const Status = {
	idle: 'IDLE',
	loading: 'LOADING',
	success: 'SUCCESS',	
	error: 'ERROR'
}

function toQueryString(params) {
  return Object.keys(params)
    .map((key) => key + "=" + params[key])
    .join("&");
}

export function useMailChimp(url) {
  const [status, setStatus] = React.useState(Status.idle);
  const [error, setError] = React.useState(null);
  const [value, setValue] = React.useState(null);

  const subscribe = React.useCallback((data) => {
    const params = toQueryString(data);
    const ajaxURL = url.replace("/post?", "/post-json?");
    const newUrl = ajaxURL + "&" + params;

    setError(null);
    setStatus(Status.loading);

    jsonp(newUrl, { param: "c" }, (err, data) => {
      if (err) {
        setStatus(Status.error);
        setError(err);
      } else if (data.result !== "success") {
        setStatus(Status.error);
        setError(data.msg);
      } else {
        setStatus(Status.success);
        setValue(data.msg);
      }
    });
  }, []);

  return { subscribe, status, error, value };
}
```

That's it! Our hook is ready to be used in our app. Here is an example of how you could use it in your app. 

```jsx
import React from 'react'
import {useMailchimp, Status} from 'hooks'

const SubscribeForm = () => {
  const [form, setForm] = React.useState({firstName: '', email: ''})
  const {subscribe, status, error, value} = useMailchimp('YOUR_MAILCHIMP_URL')

  const handleInputChange = event => {
    const target = event.target
    const name = target.name
    const value = target.value

    setForm(form => ({
      ...form,
      [name]: value,
    }))
  }

  const handleSubmit = () => {
    // We are performing some simple validation here.
    // I highly recommend checking a fully-fledged solution
    // for forms like https://react-hook-form.com/
    if (form.firstName === '' || form.email === '') return

    subscribe({
      FNAME: form.firstName,
      EMAIL: form.email,
    })
  }

  if (status === Status.loading) {
    return <Loading />
  }

  if (status === Status.error) {
    return <Error error={error}/>
  }

  if (value.includes('Already subscribed')) {
    return <Success subscribed />
  }

  if (value) {
    return <Success />
  }

  return (
    <form>
      <label htmlFor="firstName">First Name</label>
      <input
        id="firstName"
        value={form.firstName}
        onChange={handleInputChange}
      />

      <label htmlFor="email">Email</label>
      <input id="email" value={form.email} onChange={handleInputChange} />

      <button onClick={handleSubmit}>Subscribe 📨</button>
    </form>
  )
}
```

> Some of the main available names for your `data` keys are `EMAIL`, `FNAME` (first name), and `LNAME`(last name). You can find others on [Mailchimp's website.](https://mailchimp.com/)

## Getting the Serverless URL and API key

To get a URL available for a Serverless integration we need two things: 

1. The **server** your Mailchimp account is located.
2. Your **Audience List ID.**

Mailchimp's API route is as follows: 

```jsx
https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members
```

To get your **server**, you just have to check the subdomain you use to access and check your account. 

![Arrow pointing to url which contains the server](https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597480978/kick-start-your-newsletter-mailchimp-custom-form-with-react/mailchimp__6_bg6v9x.png)

For your Audience List ID, click **Audience** and then **All contacts**. 

![Select all contacts](https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597481017/kick-start-your-newsletter-mailchimp-custom-form-with-react/mailchimp__7_u7o9xj.png)

Click your desired audience. Click the Settings drop-down and choose **Audience name and defaults**.

![Select audience name and defaults](https://res.cloudinary.com/dmq9pzw0o/image/upload/v1597481141/kick-start-your-newsletter-mailchimp-custom-form-with-react/mailchimp__8_1_yidonw.png)

In the **Audience ID** section, you’ll see a string of letters and numbers. This is your audience ID.

Once you have both of these, substitute them in the URL:

```jsx
https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members
```

For the **API key** please refer to [Mailchimp's docs](https://mailchimp.com/help/about-api-keys/) where there's a quick and beautiful tutorial.

## Creating a Serverless Function for Subscribing Users

The serverless function integration depends on your provider. However, they will look very similar to each other. That said, I will be creating the function with [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction). 

I will ask for the same properties as I did for JSONP which are the user's first name and email. Therefore, I will have an `EMAIL` and `FNAME` field. For other available properties please refer to [Mailchimp's website](https://mailchimp.com/).

Our first step is to create the fu*n*ction:

```jsx
import { NextApiRequest, NextApiResponse } from "next";

export default async (req, res) => {
 if (req.method === "POST") {
  } else {
    res.setHeader("Allow", ["POST"]);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
};
```

Then, let's add some validation with an amazingly simple validation library called [ow](https://www.npmjs.com/package/ow). It will prevent faulty provided values in the request. 

Let's add the package:

```bash
npm install ow
// or
yarn add ow
```

And in our function add some basic validation for our API key, email and first name: 

```jsx
import ow from "ow";
import { NextApiRequest, NextApiResponse } from "next";

function owWithMessage(val, message, validator) {
  try {
    ow(val, validator);
  } catch (error) {
    throw new Error(message);
  }
}

owWithMessage(
  process.env.MAILCHIMP_API_KEY,
  "MAILCHIMP_API_KEY environment variable is not set",
  ow.string.minLength(1)
);

const isEmail = ow.string.is((e) => /^.+@.+\..+$/.test(e));

export default async (req, res) => {
	try {
    console.log("> Validating input", " name: ", firstName, " email:", email);
    owWithMessage(firstName, "the name is too long", ow.string.maxLength(120));
    owWithMessage(
      email,
      "The email is invalid. Please enter a valid email address.",
      isEmail
    );
  } catch (e) {
    console.log("> Validation failed", e.message);
    res.status(403).json({ message: e.message });
  }

 if (req.method === "POST") {
  } else {
    res.setHeader("Allow", ["POST"]);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
};
```

To perform requests to Mailchimp's servers we need an API key. If you did not follow the instructions above, you can refer to [Mailchimp's docs](https://mailchimp.com/help/about-api-keys/) on how to get this key. 

We will be declaring our authorization string with the generated API KEY which will be stored in an environment variable.

```jsx
const authorization =
  "Basic " +
  Buffer.from("randomstring:" + process.env.MAILCHIMP_API_KEY).toString(
    "base64"
  );
```

Now, what's left is handling the request to Mailchimp's servers. I will be using the `fetch` API since [Next.js](https://nextjs.org/) now is able to use its server-side. For other providers, it is best to use libraries like [node-fetch](https://www.npmjs.com/package/node-fetch) or [Axios](https://www.npmjs.com/package/axios). 

Mailchimp has an interesting way of handling subscribers. Once a user has been subscribed, he cannot use the subscribe endpoint again if he were to unsubscribe and would want to subscribe again. To solve this, one has to use a PATCH request method instead of a POST. Therefore, we have to verify the status of the contact before selecting a request method.  

To check and update the status of a subscriber or member, Mailchimp requires for it to be included on the URL as an md5 hash. For this, we will be using a library called [blueimp-md5](https://www.npmjs.com/package/blueimp-md5). 

Let's translate all this to code. We will have three mini services: `checkContactStatus`, `addNewMember`, and `subscribeContact`. 

```jsx
import md5 from "blueimp-md5";

function addNewMember(email: string, firstName: string) {
  return fetch("https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization,
    },
    body: JSON.stringify({
      email_address: email,
      status: "subscribed",
      merge_fields: {
        FNAME: firstName,
      },
    }),
  });
}

function subscribeContact(email: string) {
  return fetch(
    `https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members/${md5(
      email.toLowerCase()
    )}`,
    {
      method: "PATCH",
      body: JSON.stringify({
        status: "subscribed",
      }),
      headers: {
        authorization,
      },
    }
  );
}

function checkContactStatus(email: string) {
  return fetch(
    `https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members/${md5(
      email.toLowerCase()
    )}`,
    {
      headers: {
        authorization,
      },
    }
  );
}
```

Let's connect these services in our function: 

```jsx
if (req.method === "POST") {
    try {
      const { status } = await (await checkContactStatus(email)).json();
			
			// Add the contact is there is no member associated with the given email.
      if (status === 404) {
        const message = await (await addNewMember(email, firstName)).json();
			
				// Return a Not Acceptable code when Mailchimp detects 
				// a spam email or invalid property
        if (message.status === 400) {
          message.status = 406;
          return res.status(406).json({ message });
        }

        if (message) {
          return res.status(200).json({ message });
        }
      } else if (status === "subscribed") {
        res.status(400).json({ message: "User already subscribed" });
      } else {
				// Resubcribe the user
        const message = await (await subscribeContact(email)).json();
        if (message) {
          res.status(200).json({ message });
        }
      }
    } catch (err) {
      console.log(err);
      res.status(500).json({ message: err });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
```

Now we can handle the edge cases of subscribing to our users. It is so much easier to test than using JSONP since we can stub the network calls. 

Here is the complete function: 

```jsx
import { NextApiRequest, NextApiResponse } from "next";
import ow from "ow";
import md5 from "blueimp-md5";

function owWithMessage(val, message, validator) {
  try {
    ow(val, validator);
  } catch (error) {
    throw new Error(message);
  }
}

owWithMessage(
  process.env.MAILCHIMP_API_KEY,
  "MAILCHIMP_API_KEY environment variable is not set",
  ow.string.minLength(1)
);

const isEmail = ow.string.is((e) => /^.+@.+\..+$/.test(e));
const authorization =
  "Basic " +
  Buffer.from("randomstring:" + process.env.MAILCHIMP_API_KEY).toString(
    "base64"
  );

function addNewMember(email: string, firstName: string) {
  return fetch("https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization,
    },
    body: JSON.stringify({
      email_address: email,
      status: "subscribed",
      merge_fields: {
        FNAME: firstName,
      },
    }),
  });
}

function subscribeContact(email: string) {
  return fetch(
    `https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members/${md5(
      email.toLowerCase()
    )}`,
    {
      method: "PATCH",
      body: JSON.stringify({
        status: "subscribed",
      }),
      headers: {
        authorization,
      },
    }
  );
}

function checkContactStatus(email: string) {
  return fetch(
    `https://SERVER.api.mailchimp.com/3.0/lists/LIST_ID/members/${md5(
      email.toLowerCase()
    )}`,
    {
      headers: {
        authorization,
      },
    }
  );
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { firstName, email } = req.body;

  try {
    console.log("> Validating input", " name: ", firstName, " email:", email);
    owWithMessage(firstName, "the name is too long", ow.string.maxLength(120));
    owWithMessage(
      email,
      "The email is invalid. Please enter a valid email address.",
      isEmail
    );
  } catch (e) {
    console.log("> Validation failed", e.message);
    res.status(403).json({ message: e.message });
  }

  if (req.method === "POST") {
    try {
      const { status } = await (await checkContactStatus(email)).json();

      if (status === 404) {
        const message = await (await addNewMember(email, firstName)).json();

        if (message.status === 400) {
          message.status = 406;
          return res.status(406).json({ message });
        }

        if (message) {
          return res.status(200).json({ message });
        }
      } else if (status === "subscribed") {
        res.status(400).json({ message: "User already subscribed" });
      } else {
        const message = await (await subscribeContact(email)).json();
        if (message) {
          res.status(200).json({ message });
        }
      }
    } catch (err) {
      console.log(err);
      res.status(500).json({ message: err });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
};
```

That's it! In our components, we can request our function and act accordingly to the result. For reference, here are the possible subscription states according to their HTTP status codes:

* `200` - Successfully added subscriber or resubscribed.
* `400` - User is already subscribed.
* `403` - User has sent invalid credentials to function.
* `406` - User has sent spam email or credentials that are invalid on Mailchimp
* `500` - Function has encountered an unexpected error.

Here is a quick example: 

```jsx
import React from "react";
import axios from "axios";

export const Status = {
  idle: "IDLE",
  loading: "LOADING",
  success: "SUCCESS",
  error: "ERROR",
};

async function subscribe({ firstName, email }) {
  const response = await axios.post("/api/subscribe", {
    firstName,
    email,
  });
  return response;
}

const SubscribeForm = () => {
  const [form, setForm] = React.useState({ firstName: "", email: "" });
  const [subscribeState, setSubscribeState] = React.useState({
    status: Status.idle,
    value: "",
    error: null,
  });

  const handleInputChange = (event) => {
    const target = event.target;
    const name = target.name;
    const value = target.value;

    setForm((form) => ({
      ...form,
      [name]: value,
    }));
  };

  const handleSubmit = () => {
    // We are performing some simple validation here.
    // I highly recommend checking a fully-fledged solution
    // for forms like https://react-hook-form.com/
    if (form.firstName === "" || form.email === "") return;

    subscribe({ firstName: "", email: "" }).then((res) => {
      switch (res.status) {
        case 200:
          setSubscribeState({
            status: Status.success,
            value: res.data,
            error: null,
          });
          break;
        case 400:
          setSubscribeState({
            status: Status.success,
            value: "Already Subscribed",
            error: null,
          });
          break;
        case 403:
        case 406:
        case 500:
          setSubscribeState({
            status: Status.error,
            value: "",
            error: res.data,
          });
          break;
      }
    });
  };

  if (subscribeState.status === Status.loading) {
    return <Loading />;
  }

  if (subscribeState.status === Status.error) {
    return <Error error={error} statusCode={subscribeState.status} />;
  }

  if (subscribeState.value.includes("Already subscribed")) {
    return <Success subscribed />;
  }

  if (subscribeState.value) {
    return <Success />;
  }

  return (
    <form>
      <label htmlFor="firstName">First Name</label>
      <input
        id="firstName"
        value={form.firstName}
        onChange={handleInputChange}
      />

      <label htmlFor="email">Email</label>
      <input id="email" value={form.email} onChange={handleInputChange} />

      <button onClick={handleSubmit}>Subscribe 📨</button>
    </form>
  );
};
```

## Conclusion

Mailchimp is very popular and amazing to manage your subscribers or audience. Creating a custom form in a Front-End library like React can be daunting, especially since a normal server request to Mailchimp's servers using fetch or Axios won't work. With JSONP or a serverless function, one can create an amazing experience not bound to template forms.

Choosing between JSONP and serverless function depends on how robust the implementation has to be. JSONP is best for a quick solution that requires minimal code and no serverless providers. On the other hand, a serverless function allows for a testable, robust, and safer solution. 

For more up-to-date web development content, follow me on [Twitter](https://twitter.com/Jose_R_Felix), and [Dev.to](https://dev.to/jfelx)! Thanks for reading! 😎

- - -

Did you know I have a newsletter? 📬

If you want to get notified when I publish new blog posts and receive an **awesome weekly resource** to stay ahead in web development, head over to <https://jfelix.info/newsletter>.