This probes your ability to model a conversation, manage streaming messages, and handle memory limits or truncation without breaking UX.
How would you maintain conversation state in a chat app like ChatGPT?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Definition (above the fold)
To maintain conversation state in a chat app like ChatGPT, model chat history as an append-only timeline with stable IDs, explicit status transitions, and a separate streaming draft buffer. The key is deterministic state transitions: user submit, optimistic render, stream append, finalize, retry/fail. If these transitions are implicit, you get duplicate messages, stale drafts, and broken ordering under packet delay. Interviewers want a state model, not only UI screenshots. Strong answers also define persistence boundaries and recovery behavior for offline/reconnect cases, not only happy-path streaming.
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.