Back to index

October 2, 2024

Setting Up a Mailchimp Waiting List or Newsletter Subscription with Next.js 14 and server actions

author's avatar
Soufiane

Learn how to quickly set up a Mailchimp waiting list or newsletter subscription for your product using Next.js and React Server Actions.

Prerequisites

First, you'll need a Mailchimp account. Once you've created one, gather these environment variables:

- Audience ID
- API key
- API server

These can be found in your Mailchimp dashboard (under Audience settings and API keys).

Implementation

1. Set Up Environment Variables

Create or update your .env.local file.

// .env.local

MAILCHIMP_API_KEY=<YOUR API KEY>
MAILCHIMP_AUDIENCE_ID=<YOUR AUDIENCE ID e.g., 8z2a1234qw>
MAILCHIMP_API_SERVER=<SERVER CODE e.g., us12>

2. Create the Server Action

Create a new file called handleSubscription.ts.

// actions/handleSubscription.ts

"use server";

import { z } from "zod";

// Import environment variables
const AUDIENCE_ID = process.env.MAILCHIMP_AUDIENCE_ID;
const API_KEY = process.env.MAILCHIMP_API_KEY;
const API_SERVER = process.env.MAILCHIMP_API_SERVER;

export const handleSubscription = async (prevState, formData: FormData) => {
  // Validate inputs using Zod
  const SubscribeFormDataSchema = z.object({
    email_address: z.string().email().min(1).max(100),
    status: z.string().min(1).max(100),
  });

  const parse = SubscribeFormDataSchema.safeParse({
    email_address: formData.get("email"),
    status: "subscribed",
  });

  if (!parse.success) {
    return {
      error: `${formData.get("email")} failed to subscribe`,
    };
  }

  try {
    const data = parse.data;
    const response = await fetch(
      `https://${API_SERVER}.api.mailchimp.com/3.0/lists/${AUDIENCE_ID}/members`,
      {
        body: JSON.stringify(data),
        headers: {
          Authorization: `apikey ${API_KEY}`,
          "Content-Type": "application/json",
        },
        method: "POST",
      },
    );

    if (response.ok) {
      return {
        success: true,
        message: `Subscribed successfully: ${data.email_address}`,
      };
    } else {
      const res = await response.json();

      // Handle existing members gracefully
      if (res.title === "Member Exists") {
        return {
          success: true,
          error: "You are already subscribed.",
        };
      }

      return {
        success: false,
        error: "Subscription error.",
      };
    }
  } catch (error) {
    console.error("Error in handleSubscription:", error);
    return {
      success: false,
      error: "Subscription error.",
    };
  }
};

3. Create the Newsletter Component

Create a new file called SubscriptionForm.tsx.

// components/SubscriptionForm.tsx

"use client";

import { handleSubscription } from "@/app/actions/handleSubscription";
import { useFormState, useFormStatus } from "react-dom";
import { toast } from "sonner";

export default function SubscriptionForm() {
  const initialFormState = {
    message: "",
  };

  const [formState, formAction] = useFormState(handleSubscription, initialFormState);

  // Provide user feedback using toast notifications
  if (formState?.message) {
    toast.success(formState.message);
  }
  if (formState?.error) {
    toast.error(formState.error);
  }

  return (
    <form action={formAction}>
      <label htmlFor="email">Subscribe to our newsletter</label>
      <input type="email" id="email" placeholder="Email" name="email" required />
      <SubmitButton />
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" aria-disabled={pending}>
      {pending ? "Sending..." : "Subscribe"}
    </button>
  );
}

Advanced Usage

You can extend the form to include additional fields by setting new inputs fields on your form and modifying your formData object accordingly.

// inside the server action

const mailchimpData = {
  email_address: data.email,
  status: "subscribed",
  tags: [data.tags],
  merge_fields: {
    // Add any custom fields here you defined in Mailchimp
    PRODUCTREF: data.productRef,
  },
};

Benefits

- Progressively enhanced: The form works and can be submitted without JavaScript.
- No race conditions thanks to Next.js' server actions running sequentially.
- Simplified state management without the useState/useEffect dreaded combo.
- Type-safe thanks to Zod server-side validation.

Note for React 19

The stable version of React 19 replaces useFormState from 'react-dom' with useActionState from 'react'. The underlying logic remains the same.

This implementation takes advantage of Next.js' implementation of Server Actions and React's latest features to create a robust, type-safe newsletter subscription system.

Back to index