Hung-Yi's LogoHung-Yi’s Journal

React Hook: useDebounce with Enter Key Short Circuit

See how to debounce your input events to improve the UX in your React app, plus bonus "short circuiting" behavior to skip the delay for impatient users.

In React, component state can sometimes change very quickly, for instance when a user is typing input into a controlled component. But we may not want to respond to every state change immediately. If we did, we might be triggering dozens of API calls on every keystroke or starting and canceling multiple long-running operations in quick succession. How do we avoid this?

A common pattern is debouncing behavior, which we can achieve with this hook.

function useDebounce<T>(value: T, delay: number) {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState(value)
  useEffect(
    () => {
      if (JSON.stringify(value) !== JSON.stringify(debouncedValue)) {
        // Update debounced value after delay
        const handler = setTimeout(() => {
          setDebouncedValue(value)
        }, delay)
        // Cancel the timeout if value changes (also on delay change or unmount)
        // This is how we prevent debounced value from updating if value is
        // changed within the delay period. Timeout gets cleared and restarted.
        return () => {
          clearTimeout(handler)
        }
      }
    },
    [value, debouncedValue, delay] // Only re-call effect if value or delay changes
  )
  return [debouncedValue, () => setDebouncedValue(value)] as [T, () => void]
}

This version of the useDebounce hook contains a bonus “short circuiting” behavior, which I’ve found useful in many scenarios. Say a user is typing in a search query — seasoned users will hit the Enter key when they’re done typing, so we can ignore the debouncing delay and update the value immediately, providing a more responsive user experience.

Here’s an example of how to implement the hook, including the short circuiting.

function DebouncedInputExample() {
  const [input, setInput] = useState('')
  const [debouncedInput, shortCircuit] = useDebounce(input, 1000)

  return <React.Fragment>
    <label>Input</label>
    <input
      value={input}
      onChange={e => setInput(e.target.value)}
      onKeyPress={e => e.key === 'Enter' && shortCircuit()}
      placeholder="Type something here..." />
    <label>
      Debounced Result
      <br/>
      <span className="muted">
        Updates after 1s idling or on hitting Enter
      </span>
    </label>
    <pre>
      {debouncedInput ||
        <em className="muted">No input value to display</em>}
    </pre>
  </React.Fragment>
}

And here’s the whole demo in action.

Demo is loading (requires JavaScript)...