Frontend developer focused on inclusive design

Next.js & Fluent UI: Newsletter Form

Newsletters are a great means of promoting content to an interested audience.

These notes are based on setup Next.js and FluentUI. Their purpose is to aid in creating a newsletter using Next.js, MailerLite, and the Fluent UI React library.

FluentUI

The FluentUI library is utilized to create the UI for the newsletter subscription form.

The subscription form consists of an email address input field and a call-to-action button. Additionally, the form displays a message upon submission, indicating either successful subscription or an error.

Next.js

Next.js simplifies the creation of API endpoints in React with its Route Handlers feature, which is essential for building a newsletter form that requires API communication.

Note, Route Handlers are only available inside the app directory.

To prepare Route Handler:

  1. Create api folder inside the app directory if it does not exist yet.
  2. Add a route.js file to a new folder called subscribe inside app/api/.

This is where communication with MailerLite will take place.

MailerLite

MailerLite is an email marketing software that allows to create and send newsletters to subscribers. It can be integrated with Next.js using an API.

Setup

After creating an account at MailerLite, get an API key.

As Next.js does not have newsletter functionality, special software like MailerLite must be used for that purpose. An API key enables communication between a Next.js application and MailerLite.

Upon clicking the button in the newsletter form, the user's email address will be added to a designated group. Thus, the following values are required:

  • MailerLite API key.
  • Group ID for the mailing list.

Environment variables

To avoid hardcoding MailerLite secret values into API file, set them up as environment variables.

To start, a .env.local file should be created in a root of the project for local testing purposes:

MAILERLITE_API_KEY=
MAILERLITE_GROUP_ID=

Remember to add .env.local to your .gitignore file to avoid committing secrets. Also, if deploying with Vercel, these variables need to be configured accordingly.

Create API request

With the necessary values available as environment variables, it's possible to create an API route at app/api/subscribe/route.js that adds a new subscribers to the list.

Install MailerLite Node.js SDK

First, make sure to install @mailerlite/mailerlite-nodejs :

npm install @mailerlite/mailerlite-nodejs

The @mailerlite/mailerlite-nodejs package is a Node.js library for interfacing with the MailerLite email marketing platform.

It provides a simple way to integrate MailerLite functionality into Node.js applications. The package can also be used with React and Next.js applications, as both of these frameworks are built on top of Node.js.

Then, update app/api/subscribe/route.js with the following code:

/**
 * Import the NextResponse from the Next.js to create and manipulate HTTP responses.
 */
import { NextResponse } from "next/server";

/**
 * Import the MailerLite package to interact with the MailerLite API for managing
 * subscribers and mailing lists.
 */
import MailerLite from "@mailerlite/mailerlite-nodejs";

// Initialize a new instance of the MailerLite client using the API key from the environment variables
const mailerlite = new MailerLite({
  api_key: process.env.MAILERLITE_API_KEY,
});

/**
 * Handle POST requests for subscribing a user to a mailing list.
 *
 * @param {Object} request - The incoming request object containing the user's email.
 * @returns {NextResponse} - The response object containing the status and result of the request.
 */
export async function POST(request) {
  // Extract the email from the request's JSON payload
  const { email } = await request.json();

  // Define the parameters for the MailerLite API request
  const params = {
    email: email,
    groups: [process.env.MAILERLITE_GROUP_ID],
    status: "unconfirmed",
  };

  try {
    // Attempt to create or update the subscriber in MailerLite using the provided email and parameters
    const response = await mailerlite.subscribers.createOrUpdate(params);

    // Return a success response containing the result and a message
    return NextResponse.json({
      message: "You have been subscribed!",
      data: response.data,
    });
  } catch (error) {
    // Handle errors that include a response from the MailerLite API
    if (error.response) {
      return NextResponse.json(error.response.data, {
        status: error.response.status,
      });
    } else {
      // Handle other errors and return a generic error message
      return NextResponse.json(
        {
          message: "An error occurred. Please try again.",
        },
        {
          status: 500,
        }
      );
    }
  }
}

Create Newsletter component

To gather user input, create a component that sends requests to the API:

"use client";

/**
 * Import the 'useState' hook from the 'react' library.
 *
 * 'useState' is used for creating and managing state variables in functional components.
 */
import { useState } from "react";

/**
 * Import the necessary components and utilities from the '@fluentui/react-components' library.
 *
 * 'makeStyles' is a utility for creating custom CSS-in-JS styles.
 * 'shorthands' is an object containing utility functions for shorthand CSS properties.
 * 'tokens' is an object containing design token values, such as spacing and colors.
 * 'Button' is a UI component for creating buttons.
 * 'Card' is a UI component for creating a card container.
 * 'Field' is a UI component for creating form fields.
 * 'Input' is a UI component for creating form input elements.
 * 'Spinner' is a UI component for creating a loading spinner.
 */
import {
  makeStyles,
  shorthands,
  tokens,
  Button,
  Card,
  Field,
  Input,
  Spinner,
} from "@fluentui/react-components";

/**
 * Import the 'PersonMail20Regular' icon from the '@fluentui/react-icons' library.
 *
 * This icon will be used as a visual indicator for the email input field in the form.
 */
import { PersonMail20Regular } from "@fluentui/react-icons";

/**
 * Create a custom 'useStyles' hook to define the styling for the Newsletter component.
 */
const useStyles = makeStyles({
  container: {
    maxWidth: "400px",
    width: "100%",
    ...shorthands.marginInline("auto"),
  },
  form: {
    display: "flex",
    flexDirection: "row",
    alignItems: "start",
    width: "100%",
    ...shorthands.gap(tokens.spacingHorizontalS),
  },
  field: {
    flexGrow: "1",
    "& > label": {
      clip: "rect(1px, 1px, 1px, 1px)",
      clipPath: "inset(50%)",
      height: "1px",
      width: "1px",
      ...shorthands.margin("-1px"),
      ...shorthands.overflow("hidden"),
      ...shorthands.padding("0"),
      position: "absolute",
    },
    "& .fui-Field__validationMessage": {
      marginBlockStart: tokens.spacingVerticalMNudge,

      "&:empty": {
        display: "none",
      },
    },
  },
});

/**
 * 'NewsletterForm' is a component that renders a newsletter subscription form.
 * When the user submits the form, it sends a subscription request to the server.
 *
 * @param {object} props - The properties passed to the component.
 */
export default function NewsletterForm(props) {
  // Retrieve the styles object from the 'useStyles' hook.
  const styles = useStyles();

  // Declare state variables for email, message, status, isLoading, and isSubscribed.
  const [email, setEmail] = useState("");
  const [message, setMessage] = useState("");
  const [status, setStatus] = useState("none");
  const [isLoading, setIsLoading] = useState(false);
  const [isSubscribed, setIsSubscribed] = useState(false);

  /**
   * Sends a subscription request to the server with the given email address.
   *
   * @param {string} email - The email address to subscribe.
   * @returns {Promise<object>} The response data with 'message' and 'status' properties.
   */
  const subscribe = async (email) => {
    const response = await fetch("/api/subscribe", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
    });

    const data = await response.json();

    return {
      message: data.message,
      status: response.ok ? "success" : "error",
    };
  };

  /**
   * Handles the form submission.
   *
   * @param {Event} event - The form submission event.
   */
  const handleSubmit = async (event) => {
    // Prevent the default form submission behavior.
    event.preventDefault();

    // Set the isLoading state to true to indicate the subscription is in progress.
    setIsLoading(true);

    try {
      const { message, status } = await subscribe(email);
      setMessage(message);
      setStatus(status);
      setIsLoading(false);
      setIsSubscribed(status === "success");
    } catch (error) {
      setMessage("An error occurred. Please try again.");
      setStatus("error");
      setIsLoading(false);
    }
  };

  // Render the subscription form and its containing elements.
  return (
    <Card className={styles.container}>
      <form className={styles.form} onSubmit={handleSubmit}>
        <Field
          className={styles.field}
          label="Email address"
          validationState={status}
          validationMessage={message}
        >
          <Input
            contentBefore={<PersonMail20Regular />}
            type="email"
            value={email}
            placeholder="Enter your email address..."
            onChange={(event) => setEmail(event.target.value)}
            required={true}
            disabled={isSubscribed}
          />
        </Field>

        <Button
          type="submit"
          appearance="primary"
          disabled={isSubscribed || isLoading}
        >
          {isLoading ? (
            <Spinner size="tiny" label="Subscribing" />
          ) : (
            "Subscribe"
          )}
        </Button>
      </form>
    </Card>
  );
}

After creating the component, ensure that it's imported to the page.

For example, to display the form on the home page, import the NewsletterForm component in page.js located in the app directory.

The complete source code is available on Github.