fix(#338): Fix useChat stale messages with functional state updates

- Add messagesRef to track current messages and prevent stale closures
- Use functional updates for all setMessages calls
- Remove messages from sendMessage dependency array
- Add comprehensive tests verifying rapid sends don't lose messages

Refs #338

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-05 19:08:10 -06:00
parent dcf9a2217d
commit b952c24f21
2 changed files with 390 additions and 9 deletions

View File

@@ -73,6 +73,10 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
const projectIdRef = useRef<string | null>(projectId ?? null);
projectIdRef.current = projectId ?? null;
// Track messages in ref to prevent stale closures during rapid sends
const messagesRef = useRef<Message[]>(messages);
messagesRef.current = messages;
/**
* Convert our Message format to API ChatMessage format
*/
@@ -156,15 +160,19 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
createdAt: new Date().toISOString(),
};
// Add user message immediately
setMessages((prev) => [...prev, userMessage]);
// Add user message immediately using functional update
setMessages((prev) => {
const updated = [...prev, userMessage];
messagesRef.current = updated;
return updated;
});
setIsLoading(true);
setError(null);
try {
// Prepare API request
const updatedMessages = [...messages, userMessage];
const apiMessages = convertToApiMessages(updatedMessages);
// Prepare API request - use ref to get current messages (prevents stale closure)
const currentMessages = messagesRef.current;
const apiMessages = convertToApiMessages(currentMessages);
const request = {
model,
@@ -189,9 +197,13 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
totalTokens: (response.promptEvalCount ?? 0) + (response.evalCount ?? 0),
};
// Add assistant message
const finalMessages = [...updatedMessages, assistantMessage];
setMessages(finalMessages);
// Add assistant message using functional update
let finalMessages: Message[] = [];
setMessages((prev) => {
finalMessages = [...prev, assistantMessage];
messagesRef.current = finalMessages;
return finalMessages;
});
// Generate title from first user message if this is a new conversation
const isFirstMessage =
@@ -220,7 +232,6 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
}
},
[
messages,
isLoading,
conversationId,
conversationTitle,