Remix Newsletter Subscribe

Remix Newsletter Subscribe

Handy resource route that you can use to quickly let people sign up for your revue newsletter in Remix.

app/routes/resources/newsletter.tsx
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";

import { Form, Link, useFetcher, useLoaderData, useActionData, useTransition } from "@remix-run/react";
import { useEffect, useRef, useState } from "react";
import { HiShieldCheck } from 'react-icons/hi'

export const action: ActionFunction = async ({ request }) => {
    await new Promise((res) => setTimeout(res, 1000));
    const formData = await request.formData();
    const email = formData.get("email");

    try {
      const result = await fetch('https://www.getrevue.co/api/v2/subscribers', {
        method: 'POST',
        headers: {
          Authorization: `Token ${process.env.REVUE_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ email })
      });
      const data = await result.json();
  
      if (!result.ok) {
        return json({
          error: data.error.email[0]
        });
      }
      return json({
        ...data,
        subscription: "ok",
        success: "ok"
      });  
    } catch(e) {
      return json({})
    }
};

function SuccessMessage({ children }) {
    return (
        <p className="flex items-center text-sm font-bold text-green-700 dark:text-green-400">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 20 20"
                fill="currentColor"
                className="mr-2 h-4 w-4"
            >
                <path
                fillRule="evenodd"
                d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
                clipRule="evenodd"
                />
            </svg>
            {children}
        </p>
    );
}

function ErrorMessage({ children }) {
    return (
        <p className="flex items-center text-sm font-bold text-red-800 dark:text-red-400">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 20 20"
                fill="currentColor"
                className="mr-2 h-4 w-4"
            >
                <path
                fillRule="evenodd"
                d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
                clipRule="evenodd"
                />
            </svg>
            {children}
        </p>
    );
}

export const Subscribe = () => {
    const newsletter = useFetcher();
    const ref = useRef();

    const [subscribed, setSubscribed] = useState(false);
    const [error, setError] = useState("");
    const actionData = useActionData();
    const transition = useTransition();
    const state: "idle" | "success" | "error" | "submitting" =
        newsletter.state === 'submitting'
        ? "submitting"
        : newsletter?.type === 'done' && newsletter.data.success
        ? "success"
        : newsletter?.type === 'done' && newsletter.data.error
        ? "error"
        : "idle";

    const inputRef = useRef<HTMLInputElement>(null);
    const successRef = useRef<HTMLHeadingElement>(null);
    const mounted = useRef<boolean>(false);

    useEffect(() => {
        if (state === "error") {
            setSubscribed(false);
            inputRef.current?.focus();
        }

        if (state === "idle" && mounted.current) {
            inputRef.current?.select();
        }

        if (state === "success") {
            setSubscribed(true);
            successRef.current?.focus();
        }
        console.log(newsletter);
        if (newsletter.type === "done" && newsletter.data.success) {
            setSubscribed(true);
        } else if (newsletter.type === "done" && newsletter.data.error) {
            inputRef.current?.focus();
            setError(newsletter.data.error);
            setSubscribed(false);
        }
        mounted.current = true;
    }, [state, newsletter]);

    return (
        <div className="border border-slate-300 rounded p-6 my-4 w-full dark:border-gray-800 bg-slate-50 dark:bg-blue-opaque drop-shadow-sm">
            <p className="text-lg md:text-xl font-bold text-gray-900 dark:text-gray-100">
                Subscribe to the newsletter
            </p>
            <p className="my-1 text-gray-800 dark:text-gray-200">
                Subscribe to the newsletter to stay up to date with web development, tech, articles, writing and much more!
            </p>
{/*
Get emails from me about web development, tech, and early access to new articles.
*/}            
            {!subscribed && <>
                <newsletter.Form action="/resources/newsletter" className="relative my-4" method="post">
                    <input
                        aria-label="Email for newsletter"
                        ref={inputRef}
                        placeholder="steve@apple.com"
                        aria-describedby="error-message"        
                        type="email"
                        name="email"
                        autoComplete="email"
                        required
                        className="px-4 py-2 mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 pr-32"
                        tabIndex={state === "success" ? -1 : 0}
                    />
                    <button
                        className="flex items-center justify-center absolute right-1 top-1 px-4 pt-1 font-medium h-8 bg-blue-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100 rounded w-28"
                        type="submit"
                        tabIndex={state === "success" ? -1 : 0}
                    >
                    {state === "submitting" ? "Subscribing..." : 'Subscribe'}
                    </button>
                </newsletter.Form>
                <p className="my-1 text-sm text-gray-600 dark:text-gray-200">
                    <HiShieldCheck className="mr-1 w-6 h-6 text-green-600 inline-block " />
                    <strong>No spam!!</strong> I will send you at most one mail per month <i>if that</i>.
                </p>            
            </>}
            {subscribed && <SuccessMessage>
                <h2 ref={successRef} tabIndex={-1}>
                You're subscribed!{` `}
                </h2>
                <p>Please check your email to confirm your subscription.</p>
            </SuccessMessage>}
            {error && <ErrorMessage>{error}</ErrorMessage>}
        </div>
    )
}
  

Usage

1

Create Revue Account

First, create a Revue account.

2

Find API Key

From the Integrations page, retrieve your API key at the bottom.

3

Add Environment Variables

To securely access the API, we need to include the secret with each request.

Remember: never commit secrets to git. Thus, we should use an environment variable.

4

Import the subscriber box

Since this is a resource route, it doesn't export anything by default.

So on a page, such as app/routes/index.tsx, for example:

app/routes/index.tsx
import {Subscribe} from '~/routes/resources/newsletter';

export default function Index() {
  return (
      <Subscribe />
  );
}

This will output the newsletter box, and anyone who signs up will be processed in the form itself without leaving the page.