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:
- Unregisters the old callback
- Creates a new callback with current values
- Registers the new callback
This happens automatically - you don’t need to manually clean up.
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.