All questions

Streaming Chat Store Reducer

Premium

Streaming Chat Store Reducer

A reducer is a pure function (state, action) => newState — the same shape Redux and useReducer use to make state changes predictable. Here you build the reducer behind a streaming chat: the assistant's reply does not arrive all at once, so the store has to open an empty message, append text deltas as they stream in, and finally mark it complete — all without ever mutating the previous state.

State is an array of messages, each { id, role, content, status }. Implement streamingChatStoreReducer(state, action) handling four actions:

  • ADD_USER_MESSAGE { id, content } — append a user message with status: 'done'.
  • START_ASSISTANT_MESSAGE { id } — append an assistant message with empty content and status: 'streaming'.
  • APPEND_DELTA { id, delta } — append delta to that message's content.
  • FINALIZE_MESSAGE { id } — set that message's status to 'done'.

Signature

type Message = { id: string; role: 'user' | 'assistant'; content: string; status: 'streaming' | 'done' };

function streamingChatStoreReducer(state: Message[] | undefined, action: { type: string; id?: string; content?: string; delta?: string }): Message[];

Examples

let s = streamingChatStoreReducer([], { type: 'START_ASSISTANT_MESSAGE', id: 'a1' });
// → [{ id: 'a1', role: 'assistant', content: '', status: 'streaming' }]
s = streamingChatStoreReducer(s, { type: 'APPEND_DELTA', id: 'a1', delta: 'Hi' });
// → [{ id: 'a1', role: 'assistant', content: 'Hi', status: 'streaming' }]
// A no-op action returns the exact same reference.
const state = [{ id: 'a1', role: 'assistant', content: 'x', status: 'done' }];
streamingChatStoreReducer(state, { type: 'UNKNOWN' }) === state; // true

Notes

  • Never mutate — do not push onto the array or reassign a message's fields. Return new arrays and new objects for what changed.
  • Structural sharing — messages you did not touch must keep their original reference in the returned array.
  • No-ops return the same reference — an unknown action, a delta for a missing id, or starting a message whose id already exists should return state unchanged.
  • Default state — when state is undefined, treat it as [].
  • Out of scope — deleting messages, editing user text, error states, and streaming tool calls.

FAQ

Why must the reducer be immutable?
UI frameworks decide what to re-render by comparing references. If you mutate a message in place, the array and object references stay the same and the view never updates. Returning new objects for what changed and reusing references for what did not is what makes rendering both correct and cheap.
What does structural sharing mean here?
When one message changes, you return a new array and a new object for that one message, but every other message is passed through by reference. Only the touched branch is new, so an equality check skips the untouched rows.
Why return the same state reference for no-op actions?
An unknown action, or a delta for an id that is not present, changes nothing. Returning the exact same reference lets subscribers short-circuit and avoids a needless re-render.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium