DEV Community

ryanlee
ryanlee

Posted on

Typed Email Verification States in React

Email verification UX often breaks long before the backend is actualy down. The UI says "check your inbox", polling keeps spinning, the resend button wakes up too early, and support gets screenshots with zero context. I keep seeing the same issue in React apps: we treat verification like a boolean instead of a flow with a few meaningful states.

When I changed that mindset, the code got smaller and the product got easier to debug. Instead of one isLoading flag, I model the journey from send to confirm as typed states that the screen, the poller, and the logs all understand.

Why verification screens confuse users and teams

A lot of signup flows quietly mix three concerns:

  • sending the first email
  • waiting for inbox delivery
  • confirming the token after the user clicks

That blend creates muddy UX. Users do not know whether the app is waiting, stuck, or done. Engineers do not know whether the bug lives in React, the mail worker, or the inbox check. If somebody is rushing and grabs a dummy e mail or a tempail-style inbox from memory, the noise can get worse because the run context is already fuzzy.

The biggest improvement is not visual polish. It is naming the states clearly and letting every layer use the same labels.

Model the flow as typed states first

In React and TypeScript, I like a discriminated union for this. It forces the UI to be explicit about what can happen next.

type VerifyState =
  | { kind: "idle" }
  | { kind: "sending" }
  | { kind: "sent"; email: string; sentAt: number }
  | { kind: "polling"; email: string; requestId: string }
  | { kind: "verified"; verifiedAt: number }
  | { kind: "expired" }
  | { kind: "error"; message: string };
Enter fullscreen mode Exit fullscreen mode

That looks almost too simple, but it removes a lot of accidental branching. The UI can map each state to one message, one action, and one analytics event. Product people also read it quickly, which is rarer than we admit.

Here is the reducer shape:

function verifyReducer(state: VerifyState, event: VerifyEvent): VerifyState {
  switch (event.type) {
    case "send":
      return { kind: "sending" };
    case "sent":
      return { kind: "sent", email: event.email, sentAt: Date.now() };
    case "start_poll":
      return { kind: "polling", email: event.email, requestId: event.requestId };
    case "verified":
      return { kind: "verified", verifiedAt: Date.now() };
    case "expired":
      return { kind: "expired" };
    case "failed":
      return { kind: "error", message: event.message };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

I prefer this over scattered booleans because it makes impossible combinations impossible. No more isSending && isVerified weirdness sneaking into production after one rushed refactor.

Make polling abortable and inspectable

The other half of the problem is polling. A lot of React code starts a timer and hopes for the best. That works until the user resends, changes tabs, or leaves the page open overnight.

Abortable polling is a better default:

async function waitForVerification(
  requestId: string,
  signal: AbortSignal,
): Promise<"verified" | "expired"> {
  while (!signal.aborted) {
    const res = await fetch(`/api/verify-status/${requestId}`, { signal });
    const data = await res.json();

    if (data.status === "verified") return "verified";
    if (data.status === "expired") return "expired";

    await new Promise((resolve) => setTimeout(resolve, 3000));
  }

  throw new DOMException("Polling aborted", "AbortError");
}
Enter fullscreen mode Exit fullscreen mode

In the component, start polling only after the send step returns a request id. Abort it when the component unmounts or when the user triggers a resend. That one move makes your logs cleaner and your support cases less anoying to untangle.

This kind of state clarity pairs nicely with broader workflow ideas from faster email API triage in CI and from retry-safe inbox checks. Different layer, same discipline: each attempt needs its own identity.

Where temporary inboxes actually help

For product teams, a temporary email address is most useful when it helps you observe one clean verification run. I would not center the whole architecture around it, but I do use it to separate preview, QA, and local test traffic.

If you need to create temporary mail during frontend checks, keep the mailbox tied to the same request id shown in the UI and logs. That way your team can answer simple questions fast:

  • Did this resend create a new verification email?
  • Did the inbox receive the message for the current request?
  • Did the user click a link from an older run?

For lightweight checks, a contextual disposable address can help isolate one signup path without pointing tests at personal inboxes. If you want the shortest route for manual smoke testing, tempmailso works best when the request id is visible in your frontend debug panel or network traces too.

The key point is this: inbox tooling should support the state model, not replace it. Without typed states, even a clean mailbox still leaves you guessing why the screen looks stuck.

Quick Q&A

Should every verification flow poll?

No. If your backend can push status over websockets or server-sent events, that can feel better. But polling is still fine for many apps if the states are explicit and the request can be canceled cleanly.

What should the resend button do?

It should create a new request identity, cancel the old poller, and reset the copy on screen. Reusing the previous request is where a lot of subtle bugs start, especialy when retries pile up.

Do I need a full state machine library?

Not always. For many teams, a reducer plus well-named event types is enough. Reach for a library when the flow grows cross-screen or when product rules start branching hard.

Typed states sound small, because they are. But they make verification screens calmer for users and more legible for the team shipping them. That is a very good trade if your React app sends anything important by email.

Top comments (0)