Skip to content

Commit f5a82dd

Browse files
authored
fix: typing in ChatInput no longer causes unnecessary rerenders (#136)
1 parent 4a88086 commit f5a82dd

File tree

3 files changed

+78
-38
lines changed

3 files changed

+78
-38
lines changed

src/components/Chat.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -823,15 +823,7 @@ export function Chat() {
823823
[updateAssistantText, flushTextBuffer],
824824
)
825825

826-
const {
827-
messages,
828-
input,
829-
handleInputChange,
830-
handleSubmit,
831-
isLoading,
832-
setMessages,
833-
setInput,
834-
} = useChat({
826+
const { messages, isLoading, setMessages, append } = useChat({
835827
body: chatBody,
836828
onError: handleError,
837829
onResponse: handleResponse,
@@ -885,10 +877,9 @@ export function Chat() {
885877
timestamp: getTimestamp(),
886878
},
887879
])
888-
889-
handleSubmit(new Event('submit'))
880+
append({ role: 'user', content: prompt })
890881
},
891-
[hasStartedChat, handleSubmit],
882+
[hasStartedChat, append],
892883
)
893884

894885
const handleServerToggle = useCallback((serverId: string) => {
@@ -908,16 +899,15 @@ export function Chat() {
908899
setStreamBuffer([])
909900
setStreaming(false)
910901
setTimedOut(false)
911-
setMessages([]) // Clear messages completely - initialMessage will be shown via renderEvents logic
912-
setInput('')
902+
setMessages([])
913903
setFocusTimestamp(Date.now())
914904
setUseCodeInterpreter(false)
915905
setUseWebSearch(false)
916906

917907
textBufferRef.current = ''
918908
lastAssistantIdRef.current = null
919909
pendingStreamEventsRef.current = []
920-
}, [setMessages, setInput])
910+
}, [setMessages])
921911

922912
const handleScrollToBottom = useCallback(() => {
923913
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
@@ -1190,8 +1180,6 @@ export function Chat() {
11901180
<ChatInput
11911181
onSendMessage={handleSendMessage}
11921182
disabled={isLoading || streaming}
1193-
value={input}
1194-
onChange={handleInputChange}
11951183
focusTimestamp={focusTimestamp}
11961184
/>
11971185
</div>

src/components/ChatInput.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { render, screen, fireEvent } from '@testing-library/react'
3+
import { ChatInput } from './ChatInput'
4+
5+
const setup = (props = {}) => {
6+
const onSendMessage = vi.fn()
7+
render(<ChatInput onSendMessage={onSendMessage} {...props} />)
8+
9+
const textarea = screen.getByRole('textbox', {
10+
name: 'Ask something...',
11+
}) as HTMLTextAreaElement
12+
const { form } = textarea
13+
14+
return { onSendMessage, textarea, form }
15+
}
16+
17+
describe('ChatInput', () => {
18+
it('renders the input and button', () => {
19+
setup()
20+
expect(screen.getByPlaceholderText(/ask something/i)).toBeInTheDocument()
21+
expect(screen.getByRole('button')).toBeInTheDocument()
22+
})
23+
24+
it('submits message on Enter', () => {
25+
const { onSendMessage, textarea } = setup()
26+
fireEvent.change(textarea, { target: { value: 'Hello world' } })
27+
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', charCode: 13 })
28+
expect(onSendMessage).toHaveBeenCalledWith('Hello world')
29+
})
30+
31+
it('does not submit on Shift+Enter', () => {
32+
const { onSendMessage, textarea } = setup()
33+
fireEvent.change(textarea, { target: { value: 'Hello\nworld' } })
34+
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', shiftKey: true })
35+
expect(onSendMessage).not.toHaveBeenCalled()
36+
})
37+
38+
it('disables input and button when disabled', () => {
39+
setup({ disabled: true })
40+
expect(screen.getByPlaceholderText(/ask something/i)).toBeDisabled()
41+
expect(screen.getByRole('button')).toBeDisabled()
42+
})
43+
44+
it('autofocuses', () => {
45+
const { textarea } = setup({ focusTimestamp: Date.now() })
46+
47+
expect(document.activeElement).toBe(textarea)
48+
})
49+
50+
it('clears after submit', () => {
51+
const { textarea } = setup()
52+
53+
fireEvent.change(textarea, { target: { value: 'Clear me' } })
54+
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', charCode: 13 })
55+
56+
expect(textarea).toHaveValue('')
57+
})
58+
})

src/components/ChatInput.tsx

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1-
import React, { useRef, useEffect } from 'react'
1+
import { useRef, useEffect, type KeyboardEvent } from 'react'
22
import { Button } from './ui/button'
33
import { Send } from 'lucide-react'
44
import { Textarea } from './ui/textarea'
55

66
type ChatInputProps = {
77
onSendMessage: (message: string) => void
88
disabled?: boolean
9-
value: string
10-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
119
focusTimestamp?: number
1210
}
1311

1412
export function ChatInput({
1513
onSendMessage,
1614
disabled = false,
17-
value,
18-
onChange,
1915
focusTimestamp,
2016
}: ChatInputProps) {
2117
const textareaRef = useRef<HTMLTextAreaElement>(null)
@@ -28,31 +24,28 @@ export function ChatInput({
2824

2925
useEffect(() => {
3026
if (textareaRef.current) {
31-
// Reset height to calculate the right one
3227
textareaRef.current.style.height = 'auto'
33-
// Set new height based on scrollHeight (content)
3428
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 128)}px`
3529
}
36-
}, [value])
30+
}, [textareaRef.current?.value])
3731

38-
const handleSubmit = (e: React.FormEvent) => {
32+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
3933
e.preventDefault()
34+
const form = e.currentTarget as HTMLFormElement
35+
const textarea = form.elements.namedItem('prompt') as HTMLTextAreaElement
36+
const { value } = textarea
4037

4138
if (value.trim() && !disabled) {
4239
onSendMessage(value)
43-
44-
// Reset the textarea height
45-
if (textareaRef.current) {
46-
textareaRef.current.style.height = 'auto'
47-
}
40+
form.reset()
41+
textarea.style.height = 'auto'
4842
}
4943
}
5044

51-
const handleKeyDown = (e: React.KeyboardEvent) => {
52-
// Submit on Enter without Shift
45+
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
5346
if (e.key === 'Enter' && !e.shiftKey) {
5447
e.preventDefault()
55-
handleSubmit(e)
48+
e.currentTarget.form?.requestSubmit()
5649
}
5750
}
5851

@@ -64,8 +57,7 @@ export function ChatInput({
6457
<div className="relative flex-1 flex items-center">
6558
<Textarea
6659
ref={textareaRef}
67-
value={value}
68-
onChange={onChange}
60+
name="prompt"
6961
onKeyDown={handleKeyDown}
7062
placeholder="Ask something..."
7163
required
@@ -75,15 +67,17 @@ export function ChatInput({
7567
autoCorrect="off"
7668
autoCapitalize="off"
7769
spellCheck="false"
70+
aria-label="Ask something..."
7871
className="w-full resize-none rounded-lg border-0 pr-12 text-base placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 disabled:opacity-50 transition-all"
7972
/>
8073
<Button
8174
type="submit"
8275
variant="default"
8376
className="absolute right-2 size-8"
84-
aria-label="Send message"
77+
disabled={disabled}
8578
>
86-
<Send className="size-4" />
79+
<span className="sr-only">Send message</span>
80+
<Send className="size-4" aria-hidden="true" />
8781
</Button>
8882
</div>
8983
</form>

0 commit comments

Comments
 (0)