Conversation state in chat apps is a production state-machine problem: stable message IDs, streaming draft buffers, retries, reconnects, truncation, and ordering guarantees all need explicit transitions.
Use this JavaScript interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
How would you maintain conversation state in a chat app like ChatGPT?Frontend interview answer
This JavaScript interview question tests whether you can explain Chat conversation state in production: streaming buffers, retries, and ordering bugs, connect it to production trade-offs, and handle common follow-up questions.
- Chat conversation state in production: streaming buffers, retries, and ordering bugs explanation without falling back to memorized docs wording
- State Management and Ux reasoning, edge cases, and production failure modes
- How you would answer the most likely JavaScript interview follow-up
Production state machine
Conversation state in a chat app is not just an array of messages. It is a set of explicit transitions: user submit, optimistic render, stream append, finalize, retry, reconnect, and recovery. If those transitions are implicit, you get duplicate messages, stale drafts, and ordering bugs.
Core mental model
Use a finite-state message lifecycle: pending -> streaming -> done | error. Keep canonical message entities separate from UI-only controls (composer text, active request id, autoscroll lock) so retries and reconnects are predictable.
State slice | Must-have fields | Why |
|---|---|---|
messagesById | id, role, content, status, createdAt | Stable rendering and reconciliation |
orderedIds | array of message IDs | Deterministic ordering even with retries |
activeStream | requestId, assistantMessageId, cursor | Safe chunk routing and cancellation |
historyPolicy | tokenBudget, summaryCheckpointId | Bounded memory and request size |
Runnable example #1: reducer transitions
function reducer(state, action) {
switch (action.type) {
case 'user/submitted': {
const userId = action.userId;
const assistantId = action.assistantId;
return {
...state,
orderedIds: [...state.orderedIds, userId, assistantId],
messagesById: {
...state.messagesById,
[userId]: { id: userId, role: 'user', content: action.text, status: 'done' },
[assistantId]: { id: assistantId, role: 'assistant', content: '', status: 'streaming' }
},
activeStream: { requestId: action.requestId, assistantId }
};
}
case 'assistant/chunk': {
if (action.requestId !== state.activeStream?.requestId) return state;
const id = state.activeStream.assistantId;
const prev = state.messagesById[id];
return {
...state,
messagesById: { ...state.messagesById, [id]: { ...prev, content: prev.content + action.chunk } }
};
}
default:
return state;
}
}
This pattern avoids out-of-order updates by rejecting stale chunks that do not match the active request id.
Runnable example #2: retry + finalize path
function finalize(state, requestId) {
if (requestId !== state.activeStream?.requestId) return state;
const id = state.activeStream.assistantId;
const msg = state.messagesById[id];
return {
...state,
messagesById: { ...state.messagesById, [id]: { ...msg, status: 'done' } },
activeStream: null
};
}
function failAndRetry(state, requestId) {
if (requestId !== state.activeStream?.requestId) return state;
const id = state.activeStream.assistantId;
return {
...state,
messagesById: {
...state.messagesById,
[id]: { ...state.messagesById[id], status: 'error', error: 'network_timeout' }
},
activeStream: null
};
}
Keep retry metadata on the failed assistant turn so the user can retry that exact turn without duplicating prior history.
Common pitfalls
- Appending chunks without request IDs, causing duplicate or out-of-order assistant text.
- Mutating optimistic messages in place and failing to reconcile when the server returns canonical IDs.
- Keeping unbounded history in memory and payloads, which slows rendering and increases token cost.
When to use / when not to use
Use local-only ephemeral state for short support chats or anonymous sessions. Use persisted server-backed state for cross-device continuity, analytics, moderation, and reliable retry history. Do not persist everything by default if privacy scope is unclear; define retention and redaction policies first.
Interview follow-ups
Q1: How do you prevent stale chunks after a new prompt? A: Tag every stream with a requestId and discard chunks that do not match current activeStream.
Q2: How do you keep scroll behavior usable? A: Auto-scroll only when user is near bottom; freeze auto-scroll when user reads history.
Q3: How do you handle token limits? A: Maintain rolling summaries/checkpoints and send only recent turns plus summary context.
Implementation checklist / takeaway
Define message state transitions first, enforce request-scoped streaming, then add persistence and truncation rules. Strong answers show reliability under retries, reconnects, and long sessions.
Use this as one explanation rep, then continue with the JavaScript interview questions cluster or a guided prep path.