fix(memory-quiz): prevent input lag during rapid typing in room mode

When typing rapidly in room mode, users had to type each digit
8+ times before it registered. This was caused by reading stale
state.currentInput values during rapid keypresses before React
could re-render with the optimistically updated state.

Solution: Use a ref to track the current input value and update
it immediately when keys are pressed, before waiting for the
network round-trip and React re-render.

Changes:
- Add currentInputRef to track input value immediately
- Update ref in useEffect to stay in sync with state
- Use ref instead of state.currentInput in keyboard handlers
- Clear ref immediately when accepting/rejecting numbers

This fixes the async network validation issue where local state
updates were too slow for rapid user input.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-14 19:29:53 -05:00
parent a57ebdf142
commit b45139b588

View File

@@ -10,6 +10,15 @@ export function InputPhase() {
'neutral'
)
// Use a ref to track the current input to avoid stale reads during rapid typing
// This prevents the "type 8 times" issue where state.currentInput hasn't updated yet
const currentInputRef = useRef(state.currentInput)
// Keep ref in sync with state
useEffect(() => {
currentInputRef.current = state.currentInput
}, [state.currentInput])
// Use keyboard state from parent state instead of local state
const { hasPhysicalKeyboard, testingMode, showOnScreenKeyboard } = state
@@ -107,6 +116,7 @@ export function InputPhase() {
const acceptCorrectNumber = useCallback(
(number: number) => {
acceptNumber?.(number)
currentInputRef.current = '' // Clear ref immediately
setInput?.('')
setDisplayFeedback('correct')
@@ -121,7 +131,7 @@ export function InputPhase() {
)
const handleIncorrectGuess = useCallback(() => {
const wrongNumber = parseInt(state.currentInput, 10)
const wrongNumber = parseInt(currentInputRef.current, 10)
if (!Number.isNaN(wrongNumber)) {
dispatch({ type: 'ADD_WRONG_GUESS_ANIMATION', number: wrongNumber })
// Clear wrong guess animations after explosion
@@ -131,6 +141,7 @@ export function InputPhase() {
}
rejectNumber?.()
currentInputRef.current = '' // Clear ref immediately
setInput?.('')
setDisplayFeedback('incorrect')
@@ -140,7 +151,7 @@ export function InputPhase() {
if (state.guessesRemaining - 1 === 0) {
setTimeout(() => showResults?.(), 1000)
}
}, [dispatch, rejectNumber, setInput, showResults, state.guessesRemaining, state.currentInput])
}, [dispatch, rejectNumber, setInput, showResults, state.guessesRemaining])
// Simple keyboard event handlers that will be defined after callbacks
const handleKeyboardInput = useCallback(
@@ -150,7 +161,9 @@ export function InputPhase() {
// Only handle if input phase is active and guesses remain
if (state.guessesRemaining === 0) return
const newInput = state.currentInput + key
// Use ref for immediate value, update ref right away to prevent stale reads
const newInput = currentInputRef.current + key
currentInputRef.current = newInput // Update ref immediately for next keypress
setInput?.(newInput)
// Clear any existing timeout
@@ -202,8 +215,9 @@ export function InputPhase() {
)
const handleKeyboardBackspace = useCallback(() => {
if (state.currentInput.length > 0) {
const newInput = state.currentInput.slice(0, -1)
if (currentInputRef.current.length > 0) {
const newInput = currentInputRef.current.slice(0, -1)
currentInputRef.current = newInput // Update ref immediately
setInput?.(newInput)
// Clear any existing timeout
@@ -214,7 +228,7 @@ export function InputPhase() {
setDisplayFeedback('neutral')
}
}, [state.currentInput, state.prefixAcceptanceTimeout, dispatch, setInput])
}, [state.prefixAcceptanceTimeout, dispatch, setInput])
// Set up global keyboard listeners
useEffect(() => {