Skip to content

⚡ Streaming Frontend Integration

You’ve got streaming working on your backend - now let’s make your React frontend handle those real-time responses! 🚀

Right now your chat waits for complete responses. We’re going to transform it so words appear instantly as the AI generates them, just like ChatGPT.

What we’re changing:

  • Replace the single API call with streaming connection
  • Add real-time text updating as chunks arrive
  • Enhance UI with streaming indicators and stop button
  • Keep everything else the same (your beautiful design stays!)

Current flow:

Frontend: "Hello AI!" → Backend: [5 seconds] → Frontend: "Complete response!"

Streaming flow:

Frontend: "Hello AI!" → Backend: "Hello" → "Hello there!" → "Hello there! How" → "Hello there! How can I help?"

The key difference: Instead of waiting for one complete response, we process many small chunks and build the message piece by piece.


🛠️ Step 1: Update Your State for Streaming

Section titled “🛠️ Step 1: Update Your State for Streaming”

First, let’s modify your state to handle streaming. Open your src/App.jsx and update your state:

function App() {
// 🧠 STATE: Updated for streaming
const [messages, setMessages] = useState([]) // All conversations
const [input, setInput] = useState('') // What user is typing
const [isStreaming, setIsStreaming] = useState(false) // 🆕 Is AI streaming?
// 🆕 Add a ref to control the stream
const abortControllerRef = useRef(null)
// Don't forget to import useRef at the top!
}

What’s new:

  • isStreaming - Replaces loading, tells us if AI is actively streaming
  • abortControllerRef - Lets us stop streaming if user wants to cancel

Add the import:

import { useState, useRef } from 'react' // 👈 Add useRef

🚀 Step 2: Create Helper Functions for Streaming

Section titled “🚀 Step 2: Create Helper Functions for Streaming”

Before we update the main function, let’s create some helpers that make streaming easier to understand:

// 🆕 Helper: Create empty AI message placeholder
const createAiPlaceholder = () => {
const aiMessageId = Date.now() + 1
const aiMessage = {
text: "", // Start with empty text
isUser: false,
id: aiMessageId,
isStreaming: true, // 🆕 Mark as currently streaming
}
setMessages(prev => [...prev, aiMessage])
return aiMessageId // Return ID so we can update this specific message
}
// 🆕 Helper: Read the stream and update the message
const readStream = async (response, aiMessageId) => {
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
// Decode the chunk of text
const chunk = decoder.decode(value, { stream: true })
// Add this chunk to the existing message
setMessages(prev =>
prev.map(msg =>
msg.id === aiMessageId
? { ...msg, text: msg.text + chunk } // Append new text
: msg
)
)
}
}

How these helpers work:

createAiPlaceholder() - Creates an empty AI message bubble that we’ll fill with streaming text. It’s like preparing a blank piece of paper before someone starts writing on it.

readStream() - Reads the streaming response chunk by chunk and updates the message in real-time. Think of it like watching someone type a message letter by letter.


📝 Step 3: Replace Your Send Function with Streaming Version

Section titled “📝 Step 3: Replace Your Send Function with Streaming Version”

Now let’s replace your sendMessage function with a streaming version:

const sendMessage = async () => {
// 🛡️ Guards: Prevent empty messages or double-sending during streaming
if (!input.trim() || isStreaming) return
// 📝 Prepare: Create user message (same as before)
const userMessage = { text: input.trim(), isUser: true, id: Date.now() }
setMessages(prev => [...prev, userMessage])
const currentInput = input
setInput('')
setIsStreaming(true) // 🆕 Start streaming state
const aiMessageId = createAiPlaceholder() // 🆕 Create empty AI message
try {
// 🆕 Create abort controller for cancellation
abortControllerRef.current = new AbortController()
// 🆕 Call streaming endpoint (not regular chat endpoint!)
const response = await fetch('http://localhost:8000/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: currentInput }),
signal: abortControllerRef.current.signal, // 🆕 Allow cancellation
})
if (!response.ok) throw new Error('Failed to get response')
// 🆕 Read the stream and update message in real-time
await readStream(response, aiMessageId)
// 🆕 Mark streaming as complete
setMessages(prev =>
prev.map(msg =>
msg.id === aiMessageId ? { ...msg, isStreaming: false } : msg
)
)
} catch (error) {
if (error.name !== 'AbortError') { // 🆕 Don't show error if user cancelled
console.error('Streaming error:', error)
setMessages(prev =>
prev.map(msg =>
msg.id === aiMessageId
? { ...msg, text: 'Sorry, something went wrong.', isStreaming: false }
: msg
)
)
}
} finally {
// 🆕 Cleanup: Always stop streaming state and clear abort controller
setIsStreaming(false)
abortControllerRef.current = null
}
}

Key differences from your old function:

  • Stream endpoint - Calls /api/chat/stream instead of /api/chat
  • Real-time updates - Uses readStream() to update message as chunks arrive
  • Cancellation support - Uses AbortController to let users stop streaming
  • Placeholder pattern - Creates empty message first, then fills it

Let’s add a function to stop streaming if the user wants to cancel:

// 🆕 Function: Stop streaming early
const stopStreaming = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}

Why this is useful: Sometimes AI responses are long. Users should be able to stop and ask a different question.


Now let’s update your UI to show streaming status and add a stop button. You only need to change a few parts:

const handleKeyPress = (e) => {
// 🆕 Prevent sending during streaming
if (e.key === 'Enter' && !e.shiftKey && !isStreaming) {
e.preventDefault()
sendMessage()
}
}

5B: Update Message Bubbles to Show Streaming

Section titled “5B: Update Message Bubbles to Show Streaming”

In your messages area, update the message bubble code:

{/* Message Bubble - Add streaming indicator */}
<div
className={`max-w-xs lg:max-w-md px-4 py-3 rounded-2xl ${
message.isUser
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white'
: 'bg-white text-slate-800 shadow-sm border border-slate-200'
}`}
>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.text}
{/* 🆕 Show cursor when streaming */}
{message.isStreaming && (
<span className="inline-block w-2 h-4 bg-blue-500 ml-1 animate-pulse" />
)}
</p>
</div>

What this adds: A blinking cursor (like when someone is typing) appears at the end of streaming messages.

Remove your old loading animation (the bouncing dots) since we don’t need it anymore. The streaming cursor shows progress.

5D: Update the Input Area with Dynamic Button

Section titled “5D: Update the Input Area with Dynamic Button”

Replace your input area with this streaming-aware version:

{/* Input Area - Dynamic button based on streaming state */}
<div className="bg-white border-t border-slate-200 p-4">
<div className="flex space-x-3">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type for streaming response..." {/* 🆕 Updated placeholder */}
disabled={isStreaming} {/* 🆕 Disable during streaming */}
className="flex-1 border border-slate-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-slate-100 transition-all duration-200"
/>
{/* 🆕 Dynamic button: Send or Stop */}
{isStreaming ? (
<button
onClick={stopStreaming}
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-6 py-3 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-lg"
>
<span className="w-2 h-2 bg-white rounded-full"></span>
<span className="hidden sm:inline">Stop</span>
</button>
) : (
<button
onClick={sendMessage}
disabled={!input.trim()}
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 disabled:from-slate-300 disabled:to-slate-300 text-white px-6 py-3 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-lg disabled:shadow-none"
>
<Send className="w-4 h-4" />
<span className="hidden sm:inline">Send</span>
</button>
)}
</div>
{/* 🆕 Streaming status indicator */}
{isStreaming && (
<div className="mt-3 flex items-center justify-center text-sm text-slate-500">
<div className="flex space-x-1 mr-2">
<div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div>
<div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
</div>
AI is generating response...
</div>
)}
</div>

What this adds:

  • Dynamic button - Shows “Send” normally, “Stop” during streaming
  • Status indicator - Shows bouncing dots and message during streaming
  • Better UX - Input is disabled during streaming to prevent confusion

Update your header to reflect the streaming feature:

{/* Header - Updated title */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white p-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
<Bot className="w-5 h-5" />
</div>
<div>
<h1 className="text-xl font-bold">⚡ Streaming AI Chat</h1> {/* 🆕 Updated title */}
<p className="text-blue-100 text-sm">Real-time responses!</p> {/* 🆕 Updated subtitle */}
</div>
</div>
</div>

Make sure both servers are running:

Backend:

Terminal window
cd openai-backend
npm run dev

Frontend:

Terminal window
cd openai-frontend
npm run dev

Test the streaming experience:

  1. Send a message - “Write a story about a robot”
  2. Watch words appear - You should see text streaming in real-time
  3. Try the stop button - Send a long request and click “Stop”
  4. Test responsiveness - The interface should feel instant and fluid

Success looks like: Words appearing progressively as the AI generates them, with a blinking cursor at the end of the streaming message.


Before (Regular Chat):

  • User sends message → Wait 5 seconds → Complete response appears
  • Single API call to /api/chat
  • loading state with bouncing dots
  • User has to wait for complete response

After (Streaming Chat):

  • User sends message → Words appear immediately as AI generates them
  • Streaming connection to /api/chat/stream
  • isStreaming state with dynamic button
  • Real-time text updates with streaming cursor
  • User can stop generation early

The user experience transformation: Your chat now feels as responsive as ChatGPT or any professional AI application!


❌ “TypeError: Cannot read property ‘getReader’”

  • Check you’re calling /api/chat/stream not /api/chat
  • Make sure your backend streaming endpoint is working

❌ Text appears all at once, not streaming

  • Verify your backend is sending proper streaming headers
  • Check browser network tab - should see “text/plain” content type

❌ “AbortError” appearing in console

  • This is normal when users click “Stop” - the code handles it gracefully

❌ Streaming never stops

  • Check your backend closes the stream with res.end()
  • Verify no infinite loops in your readStream function

❌ Button doesn’t change to “Stop”

  • Make sure setIsStreaming(true) is called before the fetch
  • Check that isStreaming state is updating properly


📋 Step 7: Your Complete Streaming Chat Component

Section titled “📋 Step 7: Your Complete Streaming Chat Component”

Here’s your complete updated src/App.jsx with all streaming functionality:

import { useState, useRef } from 'react'
import { Send, Bot, User } from 'lucide-react'
function App() {
// 🧠 STATE: Updated for streaming
const [messages, setMessages] = useState([])
const [input, setInput] = useState('')
const [isStreaming, setIsStreaming] = useState(false)
const abortControllerRef = useRef(null)
// 🆕 Helper: Create empty AI message placeholder
const createAiPlaceholder = () => {
const aiMessageId = Date.now() + 1
const aiMessage = {
text: "",
isUser: false,
id: aiMessageId,
isStreaming: true,
}
setMessages(prev => [...prev, aiMessage])
return aiMessageId
}
// 🆕 Helper: Read the stream and update the message
const readStream = async (response, aiMessageId) => {
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
setMessages(prev =>
prev.map(msg =>
msg.id === aiMessageId
? { ...msg, text: msg.text + chunk }
: msg
)
)
}
}
// 🔧 MAIN FUNCTION: Streaming message sender
const sendMessage = async () => {
if (!input.trim() || isStreaming) return
const userMessage = { text: input.trim(), isUser: true, id: Date.now() }
setMessages(prev => [...prev, userMessage])
const currentInput = input
setInput('')
setIsStreaming(true)
const aiMessageId = createAiPlaceholder()
try {
abortControllerRef.current = new AbortController()
const response = await fetch('http://localhost:8000/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: currentInput }),
signal: abortControllerRef.current.signal,
})
if (!response.ok) throw new Error('Failed to get response')
await readStream(response, aiMessageId)
setMessages(prev =>
prev.map(msg =>
msg.id === aiMessageId ? { ...msg, isStreaming: false } : msg
)
)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Streaming error:', error)
setMessages(prev =>
prev.map(msg =>
msg.id === aiMessageId
? { ...msg, text: 'Sorry, something went wrong.', isStreaming: false }
: msg
)
)
}
} finally {
setIsStreaming(false)
abortControllerRef.current = null
}
}
// 🛑 Function: Stop streaming early
const stopStreaming = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey && !isStreaming) {
e.preventDefault()
sendMessage()
}
}
// 🎨 UI: Complete streaming interface
return (
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-blue-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl h-[700px] flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white p-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
<Bot className="w-5 h-5" />
</div>
<div>
<h1 className="text-xl font-bold">⚡ Streaming AI Chat</h1>
<p className="text-blue-100 text-sm">Real-time responses!</p>
</div>
</div>
</div>
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-6 space-y-4 bg-slate-50">
{messages.length === 0 ? (
<div className="text-center text-slate-500 mt-20">
<div className="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Bot className="w-8 h-8 text-blue-600" />
</div>
<h3 className="text-lg font-semibold text-slate-700 mb-2">
Welcome to Streaming Chat!
</h3>
<p className="text-sm">Send a message to see real-time AI responses.</p>
</div>
) : (
messages.map(message => (
<div
key={message.id}
className={`flex items-start space-x-3 ${
message.isUser ? 'justify-end' : 'justify-start'
}`}
>
{!message.isUser && (
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-full flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4 text-white" />
</div>
)}
<div
className={`max-w-xs lg:max-w-md px-4 py-3 rounded-2xl ${
message.isUser
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white'
: 'bg-white text-slate-800 shadow-sm border border-slate-200'
}`}
>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.text}
{message.isStreaming && (
<span className="inline-block w-2 h-4 bg-blue-500 ml-1 animate-pulse" />
)}
</p>
</div>
{message.isUser && (
<div className="w-8 h-8 bg-gradient-to-r from-slate-400 to-slate-600 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-white" />
</div>
)}
</div>
))
)}
</div>
{/* Input Area */}
<div className="bg-white border-t border-slate-200 p-4">
<div className="flex space-x-3">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type for streaming response..."
disabled={isStreaming}
className="flex-1 border border-slate-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-slate-100 transition-all duration-200"
/>
{isStreaming ? (
<button
onClick={stopStreaming}
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-6 py-3 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-lg"
>
<span className="w-2 h-2 bg-white rounded-full"></span>
<span className="hidden sm:inline">Stop</span>
</button>
) : (
<button
onClick={sendMessage}
disabled={!input.trim()}
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 disabled:from-slate-300 disabled:to-slate-300 text-white px-6 py-3 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-lg disabled:shadow-none"
>
<Send className="w-4 h-4" />
<span className="hidden sm:inline">Send</span>
</button>
)}
</div>
{isStreaming && (
<div className="mt-3 flex items-center justify-center text-sm text-slate-500">
<div className="flex space-x-1 mr-2">
<div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div>
<div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
</div>
AI is generating response...
</div>
)}
</div>
</div>
</div>
)
}
export default App

What this complete component includes:

  • All state management - Messages, input, streaming status
  • All helper functions - Placeholder creation, stream reading, message sending
  • Complete professional UI - Header, messages area, input with dynamic button
  • Advanced error handling - Graceful recovery from network issues
  • Premium user experience - Visual feedback, keyboard shortcuts, cancellation
  • Responsive design - Works beautifully on mobile and desktop

Incredible work! 🎉 You’ve transformed your chat from static to streaming.

What you’ve accomplished:

  • Real-time streaming - Words appear as AI generates them
  • 🎛️ Advanced state management - Streaming states and abort controllers
  • 🎨 Dynamic UI - Buttons and indicators that respond to streaming status
  • 🛑 User control - Ability to stop streaming responses
  • 🔄 Production patterns - Proper error handling and cancellation

You now understand:

  • 🌊 Stream processing - How to handle real-time data in React
  • 🧠 Advanced React patterns - useRef, abort controllers, dynamic state updates
  • 🎯 UX best practices - Visual feedback, user control, responsive interfaces
  • 🔧 Error handling - Graceful failures and user cancellation

Your chat application now provides a professional, modern AI experience that rivals any commercial application. The streaming foundation you’ve built opens the door to even more advanced features!