# Machine-Coding: Build a Chat UI with Optimistic Updates

Source: https://www.techinterview.org/post/3233474951/machine-coding-chat-ui-optimistic-updates/
Updated: 2026-05-04 · techinterview.org

The chat UI machine-coding round tests whether the candidate can build a real-time-feeling messaging interface from scratch. The exercise covers state management, optimistic updates, scroll behavior, message status indicators, and accessibility — most of these are not obviously chat-related, which is part of why the round is challenging.

This piece walks through the implementation patterns and what interviewers grade.

## The typical prompt

- "Build a chat UI. The user types a message, presses Enter, and the message appears in the chat history."

- "Build a Slack-like chat interface using this mock send-message API. Implement optimistic updates so messages appear instantly even on slow networks."

- "Build a messaging interface where messages can be in 'sending', 'sent', or 'failed' states."

## What interviewers grade

- **Working chat send/display.** Messages appear when sent.

- **Optimistic updates.** Messages show immediately with a 'sending' indicator; transition to 'sent' on confirmation.

- **Failure handling.** Failed messages show as 'failed' with a retry option.

- **Scroll behavior.** Scroll to bottom on new messages, but only if the user was already at the bottom.

- **Input handling.** Enter sends, Shift+Enter inserts a newline, empty messages don't send.

- **Accessibility.** ARIA live region for new messages so screen readers announce them.

- **Message ordering.** Messages appear in correct chronological order even when send latencies vary.

## Implementation

### Step 1: state and structure


```
type Message = {
  id: string;
  text: string;
  status: 'sending' | 'sent' | 'failed';
  timestamp: number;
};

function ChatUI({ sendMessage }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  return (
    <div className="chat">
      <div className="messages" role="log" aria-live="polite">
        {messages.map(m => <MessageBubble key={m.id} message={m} onRetry={retry} />)}
        <div ref={messagesEndRef} />
      </div>
      <form onSubmit={handleSubmit}>
        <textarea
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={handleKeyDown}
        />
      </form>
    </div>
  );
}
```


### Step 2: optimistic send


```
async function handleSubmit(e) {
  e.preventDefault();
  if (!input.trim()) return;
  const message: Message = {
    id: crypto.randomUUID(),
    text: input,
    status: 'sending',
    timestamp: Date.now(),
  };
  setMessages(prev => [...prev, message]);
  setInput('');
  try {
    await sendMessage(message);
    setMessages(prev =>
      prev.map(m => m.id === message.id ? { ...m, status: 'sent' } : m)
    );
  } catch {
    setMessages(prev =>
      prev.map(m => m.id === message.id ? { ...m, status: 'failed' } : m)
    );
  }
}
```


Optimistic update means the message shows in the UI immediately with status 'sending'. When the API confirms, status becomes 'sent'. If it fails, status becomes 'failed'.

### Step 3: keyboard handling


```
function handleKeyDown(e) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    handleSubmit(e);
  }
  // Shift+Enter inserts a newline naturally (default behavior)
}
```


### Step 4: scroll behavior

The hard requirement: scroll to bottom on new messages, but ONLY if the user was already at the bottom. If the user has scrolled up to read history, don't yank them down.


```
const [autoScroll, setAutoScroll] = useState(true);

function handleScroll(e) {
  const el = e.currentTarget;
  const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
  setAutoScroll(atBottom);
}

useEffect(() => {
  if (autoScroll && messagesEndRef.current) {
    messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
  }
}, [messages, autoScroll]);
```


The 50px threshold accounts for slight scroll offsets — exact bottom matching is brittle.

### Step 5: failure retry


```
async function retry(messageId: string) {
  setMessages(prev =>
    prev.map(m => m.id === messageId ? { ...m, status: 'sending' } : m)
  );
  const message = messages.find(m => m.id === messageId);
  if (!message) return;
  try {
    await sendMessage(message);
    setMessages(prev =>
      prev.map(m => m.id === messageId ? { ...m, status: 'sent' } : m)
    );
  } catch {
    setMessages(prev =>
      prev.map(m => m.id === messageId ? { ...m, status: 'failed' } : m)
    );
  }
}
```


## Common pitfalls

- **No optimistic update.** Waiting for the API before showing the message. Feels slow on poor networks.

- **Always scrolling to bottom.** Yanks the user down even when they're reading history.

- **No failure handling.** Failed messages disappear silently or stay in 'sending' forever.

- **Race condition on out-of-order responses.** If message B sends faster than message A, the order can flip without proper handling.

- **No ARIA live region.** Screen readers don't announce new messages.

- **Empty messages send.** Trim input and check for empty before submitting.

- **Enter+Shift handling missing.** Most chat apps allow newlines via Shift+Enter.

## Stretch goals

- Typing indicators for the other party.

- Message editing and deletion.

- Group chat with presence.

- Read receipts / message status (delivered, read).

- Markdown or rich-text formatting.

- File / image upload.

- Message search.

## Time budget for a 45-60 minute round

- 0-5 min: clarify requirements (optimistic? failure handling? group vs 1:1?).

- 5-15 min: basic UI structure with message list and input.

- 15-25 min: optimistic send with status states.

- 25-35 min: scroll behavior, keyboard handling.

- 35-45 min: failure retry, accessibility, edge cases.

- 45-60 min: stretch goals if time.

## Frequently Asked Questions

### How is this round different from a basic React form?

The chat UI tests state management for asynchronous operations, scroll handling, optimistic UX patterns. A basic form is one input one submission; chat is a list with state, async, and UX considerations.

### Should I implement WebSocket simulation?

Generally no — the interviewer provides a mock API. WebSocket integration is too much for a 45-minute round.

### What about virtualization for long chat histories?

Stretch goal. Mention if you have time; most interviews don't expect virtualization in this round.

### How important is the optimistic update?

Very. Modern messaging apps all use optimistic updates. Without it, the chat feels slow and dated.

### What's the most-missed accessibility item?

The ARIA live region. Screen readers cannot tell that new content arrived without it.
