React Forum
Blog & Guide Responses
Jan 14, 2023 · 04:00 PM

[Guide] Build a Customer Support Inbox - implementation notes

Started by Keira Santos
guidesupportroute groupsgroup contractuseGroupStatetriage scorestored derived queues

The Support Inbox guide is the clearest "route groups as shared state" example so far: queue/thread/composer share a group-level filter contract, global route owns selection + layout posture, and per-panel documents own tabs and drafts. It also pushes stored derived triage surfaces (triageById, previewLinesById, grouped queues) so the queue stays readable even while data is refreshing.

Read the guide

How did you decide what goes into group state vs global route state vs a thread document? Did stored derived triage scores actually make the queue feel calmer, or did it add too much derivation maintenance? How did you keep compose overlays from becoming dead ends (stale selection, missing thread id, etc.)? What evidence did you render so screenshots explain filters/selection/compose posture?

Comments (22)

Back to latest
Marisol Vega
Jan 18, 2023 · 02:10 AM

Route groups finally made sense to me after this guide.

Filters are shared truth across panels; selection is global motion; drafts are local truth.

Dmitri Kline
Jan 26, 2023 · 03:55 PM

We modeled it almost exactly like the guide (group filters + global selection).

The key was rendering group filter evidence in the shell so you could tell what the queue was actually showing.

Camille Duarte
Feb 08, 2023 · 06:44 AM

Counterpoint: group state can become another global store if you're not careful.

If every feature adds a group key, you end up with a giant shared filter object nobody owns.

Keira Santos
Feb 11, 2023 · 05:12 PM

Agree. We treated the group contract like an API: limited keys, defaults, and every key rendered as evidence somewhere.

If a key isn't visible (even in debug mode), it doesn't belong in the group contract.

Grace McCoy
Feb 23, 2023 · 09:30 AM

Triage scores were worth it because they prevented the queue from feeling random.

Even if the score isn't perfect, consistency is a UX feature.

Jasper Nguyen
Mar 10, 2023 · 03:05 AM

A small snippet for group state + evidence that matched the guide's posture:

tsx
export function SupportShell({ children }: { children: React.ReactNode }) {
  const route = useGlobalRoute('support', { selectedId: null, panel: 'split', density: 'comfortable' });
  const group = useGroupState('support', { status: 'open', assigned: 'any', channel: 'any', q: '', sort: 'triage' });
  return (
    <section
      data-status={group.read('status')}
      data-assigned={group.read('assigned')}
      data-channel={group.read('channel')}
      data-selected={String(route.read('selectedId'))}
    >
      {children}
    </section>
  );
}
Nina Kapoor
Mar 28, 2023 · 06:54 PM

Compose overlay dead ends were solved by self-correcting navigation in render.

If compose is open and there's no selected thread, close compose and bounce to the queue. It's strict but avoids broken screens.

Owen Price
Apr 15, 2023 · 10:30 AM

We stored previewLinesById and it made list rendering cheap and consistent.

If each row computes a preview from the last message, you get drift when data updates mid-scroll.

Benji Rios
May 06, 2023 · 04:40 AM

A misunderstanding I had: I thought route groups were just fancy context.

But the key difference is that group truth is treated as a routing contract, not just a convenience.

Lina Ortega
May 28, 2023 · 09:12 PM

We found it helpful to store derived groupedByStatus so queue sections were stable and predictable.

Otherwise a thread can "jump" between groups during a refresh and it looks like a bug.

Amina Farouk
Jun 19, 2023 · 01:01 PM

Derivation maintenance was solved by centralizing: whenever queue data changes, run deriveQueue() once and write all derived keys.

We stopped doing per-row derivations in render and the UI got calmer.

Theo Bennett
Jul 11, 2023 · 06:05 AM

One place route groups helped: shared filters between queue and thread made back/forward behavior sane.

You don't get the situation where the queue is filtered but the thread view isn't aware of it.

Sofia Valdez
Aug 02, 2023 · 10:10 AM

Counterpoint: storing triage scores can hide when the underlying data changed.

We had to also store a triageDerivedAt timestamp so people didn't assume the score was live.

Marisol Vega
Aug 06, 2023 · 07:18 PM

Same. We rendered data-triage-at on the queue header behind a flag.

It made it obvious when a refresh hadn't happened yet.

Priya Liao
Sep 09, 2023 · 02:33 AM

Evidence wise we rendered data-panel, data-density, plus the group filter keys.

Support screenshots became actionable because they included the posture implicitly.

Tomas Rivera
Oct 27, 2023 · 10:09 PM

We also used route group middleware to stamp filters into a request context for logging.

It made server logs line up with what the UI was showing.

Hana Saito
Jan 14, 2024 · 07:12 AM

The guide's posture that duplicate fetching is acceptable is important here.

Inbox UIs feel broken when the queue is fresh but the thread is stale (or vice versa). Let panels choose freshness.

Evan Brooks
Mar 21, 2024 · 03:30 AM

We used sort=triage|newest as a group key and it was great.

The queue can switch ordering without rewriting routes, and it still stays shareable because the group contract is stable.

Mina Calder
Jun 11, 2024 · 05:00 PM

Docs tie-in: this guide is basically "routing contracts" applied to support tooling.

Related: Routing and Navigation and State Management (Best Practices).

Inez Park
Sep 02, 2024 · 09:45 PM

We added a derived staleSelectionReason string when the shell clears a selection.

It helped explain why a thread "closed" (e.g. filter changed, thread no longer visible).

Jules Kwon
Jan 08, 2025 · 02:01 AM

The best part about group contracts is that they prevent the shell from becoming a prop API.

You can share state across panels without turning every component into an argument list.

Rowan Pierce
May 17, 2025 · 12:02 PM

We stored a derived "queue health" number (missing previews, missing triage, empty threads).

It caught a bunch of drift bugs early because it was an always-visible red flag in debug mode.

Salma Qureshi
Oct 11, 2025 · 04:18 AM

If you're adopting this pattern, start by defining the group contract keys clearly.

Once the shared filter story is stable, the rest of the architecture falls into place.

Ari Novak
Jan 06, 2026 · 04:01 PM

This is the first guide where the routing model genuinely explains the UX, not just the file structure.