How to Prevent Duplicate Approval Emails in React

Stop haunted approval flows: dispatch commands, not effects. Implement idempotent API and outbox email delivery. Practical React & Node.js code.

miércoles, 29 de julio de 2026 • 4 min read • Q2BSTUDIO Team

Cómo evitar duplicados en flujos de aprobación

When a product team designs an approval flow, it all seems simple: the user clicks 'Approve', the system records the decision, and a confirmation email is sent. However, when implemented in React, reality often looks very different. Uncontrolled side effects, state rehydrations, and re-renders duplicate notifications, generating customer complaints and confusing logs. This problem, more common than it seems, has a solution that goes beyond fixing a useEffect: it requires rethinking how we separate user intent from side-effect logic.

At Q2BSTUDIO, a company specialized in custom software development, we have seen dozens of projects where the same pattern causes havoc. The temptation to use an effect to react to a state change —for example, when the status property becomes approved— leads to, after a refetch or tab restore, the email being fired twice. This is not an unlikely bug; it is inevitable if the frontend decides when to send the email.

The robust alternative is to treat the approve action as an explicit event, not as a local state transition. The button dispatches a request with an idempotency key, the backend records the event in an outbox table, and a decoupled worker sends the email only if that event is new. React is limited to handling the interaction and updating the UI from fresh server state. The result: zero duplicate emails and perfect traceability in logs. This approach naturally fits with modern cloud architectures, such as those we implement with cloud services on AWS/Azure and integrated artificial intelligence, where notification reliability is critical.

The question that arises is: why do we keep seeing useEffect as the workflow orchestrator? Because it seems harmless and fast. But it is not when the component unmounts, remounts, or receives new props. The effect function runs every time its dependency changes, and if the server responds with an updated state after the mutation, the change repeats, triggering another call. Many teams try to patch it with flags or refs, but that adds complexity and remains fragile. At Q2BSTUDIO we recommend a mindset shift: let the frontend only express intentions, and let the backend be the sole owner of business decisions, including email sending.

Implementing this separation does not require a complex architecture. Simply add a unique idempotency key in each approval request, generated on the client (e.g., with crypto.randomUUID()). The server checks if that key has already been processed; if so, it responds as a duplicate without executing the email logic. The database transaction writes to an outbox table the pending email event. An independent worker —or a queue system like SQS on AWS— consumes that table and sends the email. Thus, even if the user clicks multiple times or the page reloads, only one notification occurs. This pattern is also the foundation for building AI agents that make decisions based on reliable events, without duplication.

Another key aspect is user experience. By decoupling the sending decision from the UI, we can optimize immediate feedback: disable the button during the request, show a success toast or even a progress indicator, without fear of generating side effects. If the backend responds with duplicate: true, the UI can display a friendly message like 'You have already approved this request'. This separation also facilitates integration with BI / Power BI tools, where approval events become clean metrics for productivity dashboards.

From a cybersecurity perspective, idempotency prevents an attacker from repeating an approval request and saturating the email system or causing unwanted actions. Each request carries a unique key that the server validates, and the immutable database record provides a complete audit trail. Moreover, using a transactional outbox prevents email sending from being lost or duplicated even if the worker fails. This design aligns with the resilience principles we apply in cloud-native projects.

To test this flow before release, manual clicking is not enough. We recommend three checks: 1) API test: send two requests with the same idempotency key and verify that only one outbox event is generated. 2) UI test: simulate a quick double-click and verify that the button disables and no second call is made. 3) Page reload test after approval, confirming nothing is resent to the backend. At Q2BSTUDIO, we use these tests as part of our methodology for process automation developments, ensuring each release maintains quality.

The business impact is direct: duplicate emails erode trust and increase support volume. According to the State of JavaScript 2024 survey, reliability and developer experience are key factors in tool adoption. An approval flow that never fails in its notifications is invisible to the user, but evident in the absence of complaints. Companies that outsource custom software development to us often mention that this stability allows them to scale without fear of communication incidents.

In summary, the path to avoiding effect loops in React is not about adding more conditional logic to the frontend, but moving the responsibility of email sending to the backend with an event-driven and idempotent design. This philosophy, applied at Q2BSTUDIO, extends to any system requiring reliable notifications: from document approvals to AI agent workflows or cloud platform integrations. The next time an approval flow seems 'haunted', remember that the most robust solution is also the most boring: let the frontend just click and the backend decide what to do with that click.

A BREAK?

Play for a moment before you go

OUR SERVICES

How we can help you

Do you have a project in mind?

Tell us your vision and we'll turn it into a software solution. Whatever the scope, we make your idea real.