Skip to content
Skip to main content

Build a unified inbox in React

Last updated:

You’re building a React dashboard, and one of the screens is an inbox. Your users connect a Gmail account and an Outlook account, then expect both to land in the same list, newest first, updating on their own when new mail arrives. Wire that against the Gmail API and Microsoft Graph separately and your React app carries two fetch clients, two response shapes, and two push systems. This recipe keeps the frontend simple by putting one schema and one endpoint behind the UI.

The split that matters: React owns the rendering, the Email API owns the data. You fetch messages per connected account from GET /v3/grants/{grant_id}/messages, merge them in your own code, render a list, and keep it live with a message.created webhook instead of polling. The component code stays small because the response is identical for every provider.

How do I build a unified inbox inside a SaaS product using an email API?

Section titled “How do I build a unified inbox inside a SaaS product using an email API?”

A unified inbox in a React product is an application-side merge over one endpoint. Each connected account is a grant, and you call GET /v3/grants/{grant_id}/messages once per grant, then combine the results. There’s no single cross-grant list call, so you fan out per account and sort the merged array by date.

The data layer does the heavy lifting so your components don’t branch on provider. A Gmail message and an Outlook message return the same id, subject, from, date, and folders fields, so one React row component renders any of the 6 supported providers: Google, Microsoft, Yahoo, iCloud, IMAP, and Exchange (EWS). The default page size is 50 messages and the maximum is 200, so a two-account view holds up to 100 rows per refresh before you page. Keep the source grant_id on each message so a reply action routes back to the right mailbox. For the merge and sort logic in plain JavaScript, see build a unified inbox.

The list starts server-side: call the List Messages endpoint once per grant from your backend, never from the browser, so your <NYLAS_API_KEY> stays off the client. The request below pulls the 25 most recent messages for one account. The response uses one schema across all 6 providers, so a single React component renders every row.

The date field is a Unix timestamp in seconds on every message, so one numeric sort orders the combined list correctly across providers. Your React app fetches /api/inbox and gets a single, pre-sorted array.

With the merged array in hand, the React component is ordinary list rendering. Fetch the backend route in a useEffect, hold the messages in state, and map each one to a row. Because the schema is unified, the row reads subject, from, and date the same way whether the message came from Gmail or Outlook, so there’s zero provider branching in the view.

Key the row on grantId plus id, because message IDs are only unique within one grant. The unread flag is a boolean on every message, so the bold-unread style works across all 6 providers without a per-provider lookup.

How do I add real-time email sync to a React application?

Section titled “How do I add real-time email sync to a React application?”

Subscribe a webhook to the message.created trigger and push new mail to the browser, instead of polling the API on a timer. Nylas fires message.created within seconds of mail arriving on any connected account, sending a JSON payload that carries the grant_id and the message object. One subscription covers all 6 providers, so your React app never re-fetches the whole list to find one new message.

Polling from React feels simple but scales badly. A dashboard that re-fetches every account every few seconds burns through provider rate limits, which apply per grant: Google throttles per user and per project, Microsoft per mailbox. Webhooks invert that. Your backend receives the event, writes the message to its store, and forwards it to the open browser tab over a WebSocket or Server-Sent Events channel. The React side just prepends the new row to state. Webhook payloads cap at 1 MB, and your endpoint must return 200 OK within 10 seconds or the API treats the delivery as failed and retries. For the challenge handshake, signature verification, and retry behavior, see real-time webhooks.

How do I add real-time email sync to a React-based SaaS dashboard?

Section titled “How do I add real-time email sync to a React-based SaaS dashboard?”

Register one webhook for your whole dashboard, not one per user. A single POST /v3/webhooks/ subscribes an HTTPS endpoint to message.created across every grant your application connects, so onboarding a new account needs no new subscription. The payload’s grant_id tells your backend which user’s inbox to update, and a WebSocket fan-out delivers the row to that user’s open tab.

The request below creates one subscription for the entire product. The notification_email_addresses receive alerts if delivery starts failing, which matters when one endpoint backs thousands of mailboxes. Add message.updated to the same trigger_types array to catch read-state and folder changes too.

When the event lands, write the row keyed on message id so a redelivered event doesn’t duplicate, then push it to the matching tab. The browser prepends one row, no full re-fetch.

Both approaches can keep a React inbox fresh, but they scale differently. Polling re-asks the API on a timer and spends most requests confirming nothing changed. Webhook-driven sync waits for the API to push only the deltas, which is why it wins once a dashboard holds more than a handful of accounts. The table compares them on the points that decide a SaaS build.

ConcernPolling from ReactWebhook-driven sync
Request volumeN grants times every poll intervalOne event per actual new message
Latency to UIUp to the poll intervalSeconds after mail lands
Rate-limit pressureHigh; per-grant limits hit fastLow; near-zero idle traffic
API key exposureRisky if polled from browserKey stays on the backend
New message detectionDiff the whole listgrant_id plus message in payload

A 1,000-user dashboard where each person connects 3 accounts is 3,000 grants. Polling all of them every minute is 3,000 requests a minute doing mostly nothing, while webhooks push only the messages that actually arrive. Reserve polling for a quick prototype with one or two test accounts.

Things to know when building the React layer

Section titled “Things to know when building the React layer”

A unified inbox in React is an aggregation your app owns, not a single API feature, so a few frontend decisions matter once you pass two test accounts. The points below cover what surfaces in a real dashboard build.

Never call the API from the browser. The <NYLAS_API_KEY> is a server credential. Put every List Messages and webhook call behind your own backend route, and let React talk only to that route. This keeps the key off the client and lets you cache the merged inbox per user.

Render from a local index, not live fetches. Fetching and merging on every mount gets slow once a user has several accounts. Store normalized message metadata (id, grant_id, date, subject, from, unread, folders) in your database, render React from that index, and keep it current with the message.created webhook. One table holds messages from all 6 providers because the schema is shared.

Stream updates, don’t re-poll from useEffect. Push new rows over WebSocket or Server-Sent Events so the component prepends one item. A setInterval re-fetch in useEffect recreates the polling cost the webhook just removed, and it fights React’s render cycle.

Paginate per grant. Each account advances its own page_token cursor; there’s no shared cursor across grants. The maximum page size is 200, so a “load more” button pulls the next batch from each grant, merges, and appends. Track one cursor per grant_id.

Deduplicate self-sent threads. When a user mails between their own connected accounts, the same conversation shows in two mailboxes. Group on thread_id within each grant or on normalized subject and participants so the React list doesn’t show two rows for one message.