DEV Community

ryanlee
ryanlee

Posted on

React Mention Emails Without Double Sends

Mention emails feel tiny until they ship twice. Then support gets screenshots, users mute the thread, and the product team starts wondering if the notification system can be trusted at all. I have seen this happen in React apps where the UI looked fine, but one optimistic update and one retry path quietly triggered two outbound jobs.

The fix usually is not some giant rewrite. It is a narrow contract between the React action, the API payload, and the mail worker so every mention event is created once and traced once. That sounds almost too simple, but it catches the bug class pretty fast.

Why mention emails get duplicated

Mention flows often cross more boundaries than we first think:

  • a React composer updates local state
  • a debounced save sends the draft
  • a mutation confirms the comment
  • a background worker renders the notification email

If those steps do not share one stable event id, duplicate sends sneak in. The common smell is that the UI retries after a slow network response while the server already accepted the first request. The user sees one comment, but the worker sees two jobs.

This gets messier in preview envs where people use a create temporary mail flow for fast checks. Someone grabs a dummy e mail, reruns the scenario, and now there are three notifications in the inbox with almost the same content. The test still "passes" because an email arrived, even though the system behavior is off by one.

The React state edge that causes double sends

The bug I keep finding sits around optimistic UI plus derived recipients. A comment box resolves @mentions from current editor state, then a follow-up render recomputes them after the mutation settles. If the send logic lives in both places, you get two mail intents from one user action.

I prefer turning that into one explicit server contract:

  1. The client submits one eventId with the final mention list.
  2. The API stores that eventId with the comment write.
  3. The worker refuses to send if that same eventId was already processed.

In other words, React can stay fast, but email delivery must stay idempotent. That tradeoff is worth it every time because duplicate notifications feel small inside code review and very loud in production.

A lightweight contract between UI and mail worker

Here is the shape I like in JavaScript:

const eventId = crypto.randomUUID();

await submitComment({
  body: "Nice catch @maya",
  mentions: ["maya"],
  eventId
});

const email = await waitForMentionEmail({
  mailbox: `mention-${eventId}@example.test`,
  timeoutMs: 90_000
});

expect(email.subject).toContain("mentioned you");
expect(email.headers["x-event-id"]).toBe(eventId);
expect(email.html).toContain("Nice catch");
Enter fullscreen mode Exit fullscreen mode

This is intentionally boring code. Boring is good here. The test proves one comment action produced one email with one stable id. If a queue retry or React rerender creates extra mail, the count check fails imediately.

For the surrounding system, I borrow the same mindset as run-scoped email assertions: isolate the mailbox for the run, make the event identifier visible in logs, and fail on ambiguity instead of guessing. The same discipline also matters in broader post-change email verification, even when the app stack is completely different.

What to assert in JavaScript tests

I do not think mention notifications need giant end-to-end suites. A small set of checks covers most real regressions:

  • exactly one email arrives for one eventId
  • the recipient list matches the final mention parser result
  • the message body references the right comment thread
  • the headers or metadata expose the same event id the API accepted
  • retries do not create a second delivered notification

That last one matters more than teams expect. According to the State of JavaScript 2025, asynchronous data flow and state management still rank high among the areas developers find painful, which tracks pretty well with why notification bugs stick around. When React state, background jobs, and inbox polling all meet in one feature, small ambiguity multiplies realy fast.

If you test with a disposable email address generator during CI or previews, make it part of the trace rather than a shortcut. Put the eventId in the mailbox name, in the job log, and in one outbound header. That makes failures readable instead of mysterious. I have even seen teams keep a temp org mail inbox open while debugging, which is fine for speed, but only if the run id still tells you which message belongs to which click.

Quick Q&A

Should the client generate the event id?

Usually yes. It gives the UI, API, and worker one shared reference from the very first click. If your backend already creates canonical request ids, you can use those instead, but keep it consistant across layers.

Is this only a React problem?

Nope. React just makes the edge easier to hit because optimistic rendering and retries are so common. Any client that can re-submit a notification intent can produce the same class of bug.

Do I need full HTML snapshot testing?

Not for this problem. I would rather assert one delivery count, one event id, and one or two message details than lock the whole template down. Snapshot-heavy tests tend to rot a bit faster than these contract checks.

That is the pattern I keep coming back to: one event id, one outbound message, one mailbox per run. It is small enough to ship this week, and it removes a really annoyng class of duplicate-email regressions before users ever notice them.

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

How do you handle edge cases where the inbox check fails, causing a potential double send? I'd love to swap ideas on this. Following for more content on optimizing state flow.