Real-Time Chat

hardApplication

A chat client looks simple but has to feel instant, never drop or duplicate a message, survive a flaky connection, and behave correctly as it scrolls. We'll use the RADIO framework, focused on the front-end concerns (the transport itself is a backend detail we only consume).

R

Requirements

Pin down the shape of the product first — 1:1 vs group chat and the freshness guarantees change the design the most.

Before designing, what should you nail down?

Functional vs non-functional

Functional: send/receive messages, see history, presence/typing. Non-functional: sending feels instant, messages stay correctly ordered and never duplicated, the UI recovers cleanly after a reconnect, and new messages are announced to assistive tech.

A

Architecture

Put the connection lifecycle and message state in a hook; keep the list and bubbles presentational. The list owns scroll behaviour because that's where the tricky UX lives.

Click a node to see what it owns.

<Chat>

Does: Composition root for one conversation.

State: none (delegates to useChat)

useChat(conversationId)

Does: Owns the socket lifecycle, message store, send/queue/reconcile, and presence.

State: byId, order, connectionStatus, outbox (unsent)

Encapsulating the socket here means components never touch the transport directly.

<MessageList>

Does: Renders ordered messages; stick-to-bottom auto-scroll; aria-live for new ones; paginates older history on scroll-up.

State: scroll position, isStuckToBottom

<MessageBubble>

Does: One message with delivery status + timestamp.

<Composer>

Does: Draft input; emits send; reports typing.

<PresenceBar>

Does: Online status / typing indicators.

What transport carries the messages?

Common default

Pick when: Two-way, low-latency messaging — the default for chat.

PROS
  • Full-duplex, low latency
  • Efficient for frequent small messages
  • Supports typing/presence naturally
CONS
  • Connection + reconnection management
  • Needs heartbeat/keepalive
D

Data Model

Store messages keyed by id for O(1) updates and de-duplication, with a separate ordered list of ids for rendering. Every outgoing message gets a stable clientId before it's sent so you can match the server's echo back to the optimistic bubble.

Core types.

Loading editor…

How do you keep messages ordered and de-duplicated?

Common default

Pick when: The robust default for optimistic UIs.

PROS
  • Optimistic bubble is matched to the server echo by clientId (no duplicate)
  • serverSeq gives a single source of truth for order
CONS
  • Must generate + carry a clientId on every send
I

Interface

The hook hides the transport entirely; components just call send and render messages.

Hook + component contracts.

Loading editor…
O

Optimizations & Deep Dives

The concerns that make chat feel right: optimistic send + reconciliation, reconnection with a resend queue, ordering/de-dup, and scroll behaviour.

The signature insight: optimistic send, reconciled by clientId

Render the message instantly as sending with a client-generated id. When the server echoes it back (with its real id and sequence), match on clientId and replace the optimistic copy — don't append a second bubble. If the send fails, flip it to failed with a retry affordance. This is what makes chat feel instant without duplicating messages.

Optimistic send + auto-scroll (editable)

Messages show instantly as 'sending', flip to 'sent ✓' on a simulated ack, and a reply arrives. Scroll up and the view stops auto-jumping to the bottom.

Loading editor…

Connection resilience is the other half. On disconnect, reconnect with exponential backoff + jitter, and flush a queue of messages that were still sending:

Reconnection with backoff + resend queue (illustrative).

Loading editor…

Rounding it out:

  • Scroll behaviour: stick to the bottom for new messages only when the user is already there; otherwise show a 'new messages' affordance (the demo implements the stick logic).
  • Long histories: virtualize the message list and paginate older messages on scroll-up, preserving scroll position when older content is prepended.
  • Accessibility: wrap the list in aria-live="polite" so screen readers announce incoming messages without stealing focus.
  • De-dup on reconnect: because messages are keyed by id, replaying missed messages after a reconnect can't create duplicates.

Self-check: a production-ready chat covers…

0 / 9 covered

Going deeper (tap to reveal)