Skip to main content

Why Dependencies Matter

The useHotkeys hook automatically memoizes your callback function using useCallback internally. This means you need to specify dependencies to ensure your callback always has access to the latest values.

The Problem: Stale Closures

Without proper dependencies, callbacks can capture stale values:
import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  // ❌ Wrong: count will always be 0
  useHotkeys('space', () => {
    console.log(count) // Always logs 0!
    setCount(count + 1)
  })
  
  return <div>Count: {count}</div>
}
The callback captures count when the component first renders. Even though count changes, the callback still sees the original value.

The Solution: Add Dependencies

import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  // ✓ Correct: include count in dependencies
  useHotkeys('space', () => {
    console.log(count) // Always current value
    setCount(count + 1)
  }, [count]) // Dependency array
  
  return <div>Count: {count}</div>
}

Dependency Array Position

Dependencies can be the third or fourth parameter:
import { useHotkeys } from 'react-hotkeys-hook'

// Without options - dependencies as third parameter
useHotkeys('space', handlePress, [dep1, dep2])

// With options - dependencies as fourth parameter  
useHotkeys('space', handlePress, { preventDefault: true }, [dep1, dep2])

Using Functional Updates

Avoid dependencies by using functional state updates:
import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  // ✓ No dependencies needed with functional update
  useHotkeys('space', () => {
    setCount(prevCount => prevCount + 1)
  })
  
  return <div>Count: {count}</div>
}
This works because prevCount always has the current value.

Multiple State Variables

import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'

function Editor() {
  const [content, setContent] = useState('')
  const [isSaved, setIsSaved] = useState(true)
  
  // Include all used state variables
  useHotkeys('ctrl+s', () => {
    saveToBackend(content)
    setIsSaved(true)
  }, {
    preventDefault: true,
  }, [content]) // Only content is needed; setIsSaved is stable
  
  return <textarea value={content} onChange={e => {
    setContent(e.target.value)
    setIsSaved(false)
  }} />
}
State setters from useState are stable and don’t need to be in the dependency array.

Dependencies with Objects and Arrays

import { useHotkeys } from 'react-hotkeys-hook'
import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState([])
  const [selectedId, setSelectedId] = useState<string | null>(null)
  
  useHotkeys('delete', () => {
    if (selectedId) {
      setTodos(todos.filter(todo => todo.id !== selectedId))
    }
  }, [todos, selectedId]) // Include both
  
  return <div>...</div>
}

Using Refs to Avoid Dependencies

Use useRef for values that change but shouldn’t trigger re-registration:
import { useHotkeys } from 'react-hotkeys-hook'
import { useRef, useState } from 'react'

function Logger() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)
  
  // Update ref when count changes
  countRef.current = count
  
  // No dependencies needed - ref always has current value
  useHotkeys('l', () => {
    console.log('Current count:', countRef.current)
  })
  
  return <div>Count: {count}</div>
}

Dependencies with Props

import { useHotkeys } from 'react-hotkeys-hook'

interface EditorProps {
  documentId: string
  onSave: (id: string, content: string) => void
}

function Editor({ documentId, onSave }: EditorProps) {
  const [content, setContent] = useState('')
  
  // Include prop values in dependencies
  useHotkeys('ctrl+s', () => {
    onSave(documentId, content)
  }, {
    preventDefault: true,
  }, [documentId, content, onSave])
}

Memoized Callbacks

If you pass a callback from a parent, memoize it:
import { useHotkeys } from 'react-hotkeys-hook'
import { useCallback, useState } from 'react'

function Parent() {
  const [data, setData] = useState([])
  
  // Memoize to prevent re-creating on every render
  const handleSave = useCallback((content: string) => {
    console.log('Saving:', content)
    setData([...data, content])
  }, [data])
  
  return <Child onSave={handleSave} />
}

function Child({ onSave }: { onSave: (content: string) => void }) {
  const [content, setContent] = useState('')
  
  useHotkeys('ctrl+s', () => onSave(content), [content, onSave])
}

Context Values

import { useHotkeys } from 'react-hotkeys-hook'
import { useContext } from 'react'

function Component() {
  const { user, settings } = useContext(AppContext)
  
  // Include context values that are used
  useHotkeys('ctrl+k', () => {
    openCommandPalette(user, settings)
  }, [user, settings])
}

When Dependencies Change

When dependencies change, the hook:
  1. Unregisters the old callback
  2. Creates a new callback with current values
  3. Registers the new callback
This happens automatically - you don’t need to manually clean up.

Performance Considerations

Too Many Dependencies

If dependencies change frequently, consider refactoring:
import { useHotkeys } from 'react-hotkeys-hook'

// ❌ Re-registers on every position change
function Map() {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  
  useHotkeys('c', () => {
    centerMap(position)
  }, [position]) // Changes frequently!
}

// ✓ Use ref or functional update
function Map() {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  const positionRef = useRef(position)
  positionRef.current = position
  
  useHotkeys('c', () => {
    centerMap(positionRef.current)
  }) // No dependencies
}

Common Patterns

Pattern 1: Simple State

// If you read state: include it
useHotkeys('space', () => console.log(count), [count])

// If you only update: use functional form, no deps
useHotkeys('space', () => setCount(c => c + 1))
// Include all state you read
useHotkeys('ctrl+s', () => {
  save(title, content, author)
}, [title, content, author])

Pattern 3: Frequent Changes

// Use refs for frequently changing values
const valueRef = useRef(value)
valueRef.current = value
useHotkeys('l', () => log(valueRef.current))

Pattern 4: Callbacks from Props

// Include callbacks and values they need
useHotkeys('ctrl+s', () => onSave(content), [onSave, content])
// Ensure onSave is memoized in parent with useCallback

ESLint Integration

If you use eslint-plugin-react-hooks, it will warn about missing dependencies:
// ESLint will warn if count is missing
useHotkeys('space', () => setCount(count + 1), [])
//                                  ^^^^^ missing dependency
Follow the ESLint warnings - they help prevent bugs.
Missing dependencies can cause subtle bugs where your callback uses stale values. Always include values you read from closures.
When in doubt, use functional state updates (setValue(prev => prev + 1)) to avoid dependency issues entirely.
The dependencies array works exactly like React’s useCallback and useEffect - the same rules apply.

Build docs developers (and LLMs) love