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

[Guide] Build a Chat App - implementation notes

Started by Noah Vance
guidechatuseRenderFetchpollingstreamsroute shell

The Chat guide is leaning into motion: polling and streams together, render-fetch for room reads, a global route store for room selection and mode, and a composer that keeps DOM truth and state truth in sync while still surfacing route behavior as evidence.

Read the guide

If you have built a chat-like feed, how do you decide between polling, streams, or both? How do you keep the route from re-reading too aggressively when render-fetch is part of the contract? What evidence do you render so you can tell whether the feed is fresh, lagging, or retrying?

Comments (10)

Back to latest
Mina Calder
Jan 02, 2023 · 04:06 PM

Poll plus stream seems redundant until you ship it. Streams drop. Polling is the safety net.

Dmitri Kline
Jan 02, 2023 · 04:14 PM

The key to making it manageable is making the posture visible.

If you cannot tell which lane you are in (poll-only, stream-only, both), you will debug ghosts.

Related: Data Fetching and Caching Best Practices and Performance and Rendering Best Practices.

Amina Farouk
Jan 02, 2023 · 04:22 PM

We ended up expressing the lane in route state so anyone could reproduce behavior by sharing a URL:

ts
type Lane = 'poll' | 'stream' | 'both';
const route = useGlobalRoute('chat', { lane: 'both' as Lane });

Then the shell renders data-lane and support screenshots become actionable.

Keira Santos
Jan 02, 2023 · 04:33 PM

Counterpoint: streams can cause a lot of UI churn if you render every message as it arrives.

We had to batch updates or the app felt jittery even though the data was fresh.

Noah Vance
Jan 02, 2023 · 04:39 PM

Agree. Freshness is not the only axis. We ended up batching renders and storing derived evidence like lastBatchAt.

The guide does not go deep there, but it fits the posture: keep motion, but make it legible.

Keira Santos
Jan 02, 2023 · 04:44 PM

Batching + evidence is a good combo. Otherwise you just get a fast app that feels chaotic.

Hana Saito
Jan 02, 2023 · 04:55 PM

On render-fetch aggressiveness: we had to define a cache posture for room summaries or the route would re-read too often.

Once posture was explicit, the behavior stopped being mysterious.

Grace McCoy
Jan 02, 2023 · 05:04 PM

Composer flexibility matters. We had a composer that felt slow because every keystroke was treated as route truth.

Keeping DOM truth alive while committing state on boundaries made typing feel normal again.

Camille Duarte
Jan 02, 2023 · 05:18 PM

A misunderstanding I see: people assume poll and stream means duplicated messages.

If you keep a stable message id contract and store derived de-dupe evidence, it stays clean.

Rowan Pierce
Jan 02, 2023 · 05:24 PM

Do you store a separate set of ids, or just derive it from the list?

We got burned by deriving it repeatedly in render on big rooms.

Camille Duarte
Jan 02, 2023 · 05:29 PM

We stored it as derived state in the feed document.

The list stays for rendering; the id set stays for de-dupe and quick membership checks.

Salma Qureshi
Jan 02, 2023 · 05:38 PM

A tiny repro of the churn problem Keira mentioned:

tsx
stream.onMessage((m) => setMessages((prev) => [...prev, m]));

If this causes a render per message, batching becomes part of the contract. Otherwise INP goes sideways fast.

Priya Liao
Jan 02, 2023 · 05:49 PM

I would love a follow-up that connects this to route-level perf budgeting.

Chat is where you discover that freshness and calmness are competing constraints.

Benji Rios
Jan 02, 2023 · 06:04 PM

The guide is good because it keeps repeating the same principle: keep motion, but keep it legible.

If you cannot prove which lane you are in, the feed will become a pile of folklore fixes.