skip to content

A React component sends a welcome email from an effect and, after it went out twice, a teammate guarded it with a ref: `useEffect(() => { if (!sentRef.current) { sentRef.current = true; sendWelcomeEmail(userId); } }, [userId])`. Why is that guard the wrong fix, and what would you do instead?

level: seniorimportance: should knowfreq 40%

answer

  1. a guard means the trigger is wrong
  2. refs are per instance, not per user
  3. module flag fails the second signup
  4. the deps array stops telling the truth
  5. idempotency, not a client-side boolean

basics

~20 s

The guard hides a placement error: sending a welcome email is caused by the signup completing, not by a component being displayed, so it belongs in that handler or on the server. A ref is also recreated on remount, so the duplicates come back.

solid answer

~50 s

The guard treats the symptom. The email is a consequence of an event — an account was created — so it belongs in the code path that created the account, ideally on the server as part of that operation, not in a component that happens to render afterwards. Mechanically the guard is also unsound: a ref lives with a component instance, so unmounting and remounting gives you a fresh `sentRef` and a second email; a module-level flag survives remounts but then silently suppresses the email for the next user in the same session. And the guard makes the dependency array a lie — it claims the effect reacts to `userId`, yet a change of `userId` sends nothing. The right shape is to fire it from the event, and make the operation idempotent server-side with a key so retries and duplicate calls cannot produce two emails.

go deeper

for a junior

Know that a useRef value is created fresh each time a component mounts, so a ref guard cannot promise something happens only once across the app's lifetime.

for a middle

Explain both failure modes concretely — the ref resets on remount, the module flag suppresses the next user's email — and that the deps array no longer describes what the effect does.

for a senior

Diagnose it as misplaced work: name the event that actually causes the email, move the call there or into the server operation, and add idempotency so retries and duplicate requests cannot send twice.

for a principal

Set the boundary for the system: side effects with external consequences are owned by the server operation that causes them, and client effects are restricted to work that is safe to repeat — then make that reviewable rather than a matter of taste.

## What the guard is really admitting A `useRef` guard around an effect body is a statement that the effect is firing more often than the author wants. That is worth taking literally: effects are designed to run whenever the component mounts and whenever a dependency changes, and React deliberately exercises that in development. If "run again" is unacceptable for this code, then "runs when the component is displayed" was the wrong trigger in the first place. Sending an email, charging a card, creating an order, incrementing a counter — these are consequences of an event, and they have a specific cause: the signup submitted, the button was pressed, the payment confirmed. That cause exists somewhere in the code, and it is where the call belongs. ## Why the guard does not even work **A ref is per component instance.** `useRef` gives a fresh object each time the component mounts. Unmount and remount — a route change and back, a parent re-keying the subtree, a tab that unmounts hidden content — and `sentRef.current` is `undefined` again. The second email arrives, just less often, which is worse than reliably arriving because it now looks like an intermittent bug. **A module-level flag has the opposite failure.** Hoisting the flag out of the component makes it survive remounts, but it is now global to the module for the lifetime of the page. Sign up a second account in the same session, or render two instances, and someone gets no email at all — a silent failure nobody reports. **The dependency array becomes a lie.** The array says `[userId]`, which reads as "re-run for each user". The guard means only the first user is ever handled. Anyone maintaining this later has to read the body to discover that the declared reactivity is fake. **It only covers this one call site.** Any other path that mounts the component with a new user, or any retry after a failure, is outside the guard's knowledge. ## The fix, in order of preference **Move it to the server, into the operation that caused it.** The account-creation endpoint knows exactly once that an account was created. Sending the welcome email there removes the client from the question entirely — no mount, no navigation, no double render can affect it. This is the answer an interviewer is usually listening for. **If it must be client-triggered, put it in the handler.** The submit handler that created the account calls it once, after the create succeeds, with `try`/`catch` around it. One cause, one call. **Make the operation idempotent.** Networks retry, users double-click, background jobs re-run. Give the request a stable idempotency key — the user id plus the email type is often enough — and let the server drop a repeat. This is what actually guarantees "exactly once", because no client-side flag can survive a reload, a second tab, or a lost response that the client retries. ## When the trigger genuinely is "this screen was reached" Sometimes the product requirement really is arrival-based: mark a notification read when its detail page is opened, record that an onboarding step was viewed. Here an effect is the correct trigger, because the cause is being displayed. The way to make it safe is not a ref, it is idempotency: a write that sets a flag to the same value twice costs nothing, and `PUT`-shaped semantics or an idempotency key makes the duplicate harmless. Say that explicitly — the distinction is between operations where repetition is free and operations where it is money or a message to a human. ## StrictMode is the messenger Development `<StrictMode>` mounts, unmounts and remounts each component so effects run twice. It did not create this bug; it revealed that the effect is not safe to run twice, which is exactly the property React requires of effects, since a real remount can happen at any time in production. Any answer that ends with "we turned off StrictMode" or "we added a ref so the double-invoke stops" has missed the diagnosis: the correct response is either to make the operation safe to repeat or to move it out of the effect. ```js // Caused by an event: it belongs with the event. async function handleSignup(form) { const user = await createAccount(form); // server sends the welcome email navigate(`/welcome/${user.id}`); } ```

  • The product genuinely wants an event recorded when a detail page is opened. Is an effect acceptable there?
    Yes — the cause really is being displayed, so an effect is the right trigger. Make the write idempotent instead of guarding it: setting "read" to true twice costs nothing, and an idempotency key makes a duplicate harmless. The rule is not "never write from an effect", it is "an effect's work must be safe to repeat".
  • Why is a module-level flag not simply a better version of the ref?
    It swaps one failure for a worse one. It survives remounts, but it is global for the page's lifetime, so a second account created in the same session gets no email and two mounted instances share one flag. A missing email is silent, which makes it harder to find than a duplicate.
  • What would you look for to confirm this bug in production rather than in development?
    Server-side send logs grouped by user id — duplicates prove it re-fires without StrictMode. Correlate the timestamps with client navigation events; pairs a few seconds apart usually line up with a remount, such as a back-navigation or a parent re-key, rather than with two separate signups.

saying these in an interview costs you the question

  • Says the fix is to disable StrictMode in development
  • Believes a useRef value survives unmount and remount
  • Adds a module-level boolean and calls the problem solved
  • Treats the double send as a development-only artifact
  • Never asks what event actually caused the email

context