π File detail
components/FeedbackSurvey/useDebouncedDigitInput.ts
π― Use case
This file lives under βcomponents/β, which covers shared React UI pieces. On the API surface it exposes useDebouncedDigitInput β mainly functions, hooks, or classes. Dependencies touch React UI. It composes internal code from utils (relative imports). What the file header says: Delay before accepting a digit as a response, to prevent accidental submissions when users start messages with numbers (e.g., numbered lists). Short enough to feel instant for intentional presses, long enough to cancel when the user types more characters.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Delay before accepting a digit as a response, to prevent accidental submissions when users start messages with numbers (e.g., numbered lists). Short enough to feel instant for intentional presses, long enough to cancel when the user types more characters.
π€ Exports (heuristic)
useDebouncedDigitInput
π External import roots
Package roots from from "β¦" (relative paths omitted).
react
π₯οΈ Source preview
import { useEffect, useRef } from 'react'
import { normalizeFullWidthDigits } from '../../utils/stringUtils.js'
// Delay before accepting a digit as a response, to prevent accidental
// submissions when users start messages with numbers (e.g., numbered lists).
// Short enough to feel instant for intentional presses, long enough to
// cancel when the user types more characters.
const DEFAULT_DEBOUNCE_MS = 400
/**
* Detects when the user types a single valid digit into the prompt input,
* debounces to avoid accidental submissions (e.g., "1. First item"),
* trims the digit from the input, and fires a callback.
*
* Used by survey components that accept numeric responses typed directly
* into the main prompt input.
*/
export function useDebouncedDigitInput<T extends string = string>({
inputValue,
setInputValue,
isValidDigit,
onDigit,
enabled = true,
once = false,
debounceMs = DEFAULT_DEBOUNCE_MS,
}: {
inputValue: string
setInputValue: (value: string) => void
isValidDigit: (char: string) => char is T
onDigit: (digit: T) => void
enabled?: boolean
once?: boolean
debounceMs?: number
}): void {
const initialInputValue = useRef(inputValue)
const hasTriggeredRef = useRef(false)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Latest-ref pattern so callers can pass inline callbacks without causing
// the effect to re-run (which would reset the debounce timer every render).
const callbacksRef = useRef({ setInputValue, isValidDigit, onDigit })
callbacksRef.current = { setInputValue, isValidDigit, onDigit }
useEffect(() => {
if (!enabled || (once && hasTriggeredRef.current)) {
return
}
if (debounceRef.current !== null) {
clearTimeout(debounceRef.current)
debounceRef.current = null
}
if (inputValue !== initialInputValue.current) {
const lastChar = normalizeFullWidthDigits(inputValue.slice(-1))
if (callbacksRef.current.isValidDigit(lastChar)) {
const trimmed = inputValue.slice(0, -1)
debounceRef.current = setTimeout(
(debounceRef, hasTriggeredRef, callbacksRef, trimmed, lastChar) => {
debounceRef.current = null
hasTriggeredRef.current = true
callbacksRef.current.setInputValue(trimmed)
callbacksRef.current.onDigit(lastChar)
},
debounceMs,
debounceRef,
hasTriggeredRef,
callbacksRef,
trimmed,
lastChar,
)
}
}
return () => {
if (debounceRef.current !== null) {
clearTimeout(debounceRef.current)
debounceRef.current = null
}
}
}, [inputValue, enabled, once, debounceMs])
}