---
title: "State Management for AI Conversations: Beyond Redux"
description: "Conversation state, booking state, streaming state, and user preferences — all interacting in real time. Here is how we manage the complexity."
canonical: https://nowah.xyz/blog/state-management-ai-conversations
lastModified: "2026-08-07T03:49:52.780Z"
---

# State Management for AI Conversations: Beyond Redux

Conversation state, booking state, streaming state, and user preferences — all interacting in real time. Here is how we manage the complexity.

State management is the single most complex frontend challenge in building an AI chat product. I say this having worked on plenty of React apps with complicated state requirements. None of them came close to the complexity of managing state for a conversational AI that can search, compare, book, and pay within a single thread.

The problem is not that any individual piece of state is hard. It is that several state domains with fundamentally different characteristics all interact in real time. Here is how we untangled it.

## Why traditional state management breaks

![Illustration for this section](https://pics.nowah.xyz/website-media/engineering-032-img-1.webp)

In a typical web app, state is predictable. A form has fields. A page has data. State changes are discrete events triggered by user actions. You can draw a state diagram and it is finite.

AI chat conversations break every one of these assumptions.

**State is unbounded.** A conversation can have 5 messages or 500. Each message might contain structured data like flight offers, hotel results, or booking details. The total state size grows without limit.

**Updates are non-deterministic.** When the AI agent responds, you do not know in advance what type of content it will produce. It might be text. It might be flight cards. It might be both. The state management layer has to accommodate whatever arrives.

**Booking flows are interleaved.** A user might be looking at flight options, decide to search for hotels, then come back to the flights. There are concurrent state domains (flights being considered, hotels being searched, a potential booking in progress) that need to coexist without interfering.

**Streaming creates continuous updates.** During an AI response, tokens arrive many times per second. Each token technically changes state. If you treat each one as a normal state update, you drown in re-renders.

We tried a single global store for all of this early on. It worked for about two weeks of development before becoming unmanageable. State updates in one domain caused re-renders in unrelated parts of the UI. Performance degraded as conversations grew. Debug tooling became useless because every state change touched the same giant object.

## Three concurrent state domains

Our solution splits state into three domains with different lifecycles and update patterns.

**Conversation state** is the history of messages. It is append-only during a conversation. Messages arrive and they stay. This state is simple in structure (an ordered list) but large in volume. It needs to persist across sessions so users can reopen a conversation from yesterday.

**Booking state** is a state machine. At any point, the user is either not booking anything, reviewing a flight booking, reviewing a hotel booking, processing a payment, or viewing a confirmation. Transitions between states are well-defined. Only one booking can be active at a time. This state resets when the booking completes or is abandoned.

**Streaming state** is ephemeral and high-frequency. It exists only while the AI agent is actively streaming a response. It updates dozens of times per second. When the stream ends, it collapses into a single message in conversation state and disappears.

Each domain has different optimal storage and update patterns, which is why putting them all in one store was a mistake.

## How we split responsibilities

![Supporting diagram](https://pics.nowah.xyz/website-media/engineering-032-img-2.webp)

We use two state management tools, each handling what it is best at.

For client-side state (conversation UI, booking flow, streaming, user preferences), we use a lightweight store. This gives us fine-grained subscriptions. A component that only cares about booking state does not re-render when conversation state changes. A component that only cares about streaming state does not re-render when a new message is added to history.

For server-synchronized state (trip data, thread history, user profile, booking records), we use a server state library. This handles caching, background refetching, optimistic updates, and stale-while-revalidate patterns. When you navigate to your trips list, the cached data appears instantly while fresh data loads in the background.

The boundary between these two systems is clean: anything that needs to stay in sync with the server goes through the server state library. Anything that is purely client-side (current scroll position, which card is expanded, whether the voice input is active) goes in the client store.

## Streaming state in detail

Streaming state deserves its own explanation because it is the most unusual pattern.

When the AI agent starts responding, we create a streaming state container. It holds the accumulated tokens, the current message type (text, tool result, error), and metadata about the stream (started timestamp, token count, any tool calls in progress).

Tokens append to an internal buffer. We flush this buffer to the rendered state on a throttled schedule, roughly every 16ms. Components subscribe to the rendered state, not the raw buffer. This means the component tree updates at most 60 times per second regardless of how fast tokens arrive.

When the stream completes, we take the final content and create an immutable message object. This object gets appended to conversation state. The streaming state container is destroyed. From this point on, the message is just a normal entry in the conversation history.

This lifecycle prevents streaming artifacts from leaking into conversation state. If the stream errors halfway through, we can discard the streaming state cleanly without corrupting the conversation history.

## Persistence and hydration

Users expect to close the app and reopen it hours later with their conversation intact. This requires persisting conversation state and hydrating it on launch.

We persist conversation state to local storage on mobile and to the server via our API. The local copy provides instant hydration on app launch. The server copy provides cross-device continuity.

The tricky part is hydrating rich content. A message might reference flight offers that were live when the conversation happened but are no longer available at the prices quoted. We hydrate the text content and display it as-is, but we mark any pricing information as "price at time of search" to set expectations. If the user wants to book an option from a previous conversation, the agent re-checks availability and pricing in real time.

Streaming state is never persisted. If the user closes the app mid-stream, they see the partial response as a completed message when they return, and the agent can be prompted to continue.

## Performance optimization

State-heavy chat interfaces need careful performance management. Our key optimizations:

**Selective subscriptions.** Components subscribe only to the state slices they need. The message list subscribes to conversation state. The payment modal subscribes to booking state. The streaming indicator subscribes to streaming state. No component subscribes to everything.

**Memoization of message rendering.** Once a message is in conversation state, it is immutable. This means React can skip re-rendering it entirely when other state changes. A conversation with 200 messages only re-renders the newest one when a new message arrives.

**Batched updates during streaming.** As described above, we batch token updates to control render frequency. This alone reduced our render count during streaming by 80%.

**Lazy hydration.** When loading a long conversation history, we hydrate only the visible messages plus a buffer. Older messages hydrate as the user scrolls up. This keeps the initial load fast regardless of conversation length.

State management for AI conversations is a genuinely hard problem, and I do not think the industry has fully settled on best practices yet. What we have works well for our product, but I expect the patterns to keep evolving as more teams build chat-first AI products and share what they learn.

---

Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. [Plan your next trip](https://app.nowah.xyz).
