import React from 'react'
import ReactDOM from 'react-dom/client'
import {
HotkeysProvider,
formatForDisplay,
useHotkeys,
} from '@tanstack/react-hotkeys'
import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import type { Hotkey, UseHotkeyDefinition } from '@tanstack/react-hotkeys'
import './index.css'
const plugins = [hotkeysDevtoolsPlugin()]
function App() {
return (
<>
<HotkeysProvider>
<div className="app">
<header>
<h1>useHotkeys</h1>
<p>
Register multiple hotkeys in a single hook call. Supports dynamic
arrays for variable-length shortcut lists.
</p>
</header>
<BasicMultiHotkeys />
<CommonOptionsDemo />
<DynamicHotkeysDemo />
</div>
</HotkeysProvider>
<TanStackDevtools plugins={plugins} />
</>
)
}
// ---------------------------------------------------------------------------
// Basic: multiple hotkeys registered at once
// ---------------------------------------------------------------------------
function BasicMultiHotkeys() {
const [log, setLog] = React.useState<Array<string>>([])
const [saveCount, setSaveCount] = React.useState(0)
const [undoCount, setUndoCount] = React.useState(0)
const [redoCount, setRedoCount] = React.useState(0)
useHotkeys([
{
hotkey: 'Shift+S',
callback: (_e, { hotkey }) => {
setSaveCount((c) => c + 1)
setLog((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
},
},
{
hotkey: 'Shift+U',
callback: (_e, { hotkey }) => {
setUndoCount((c) => c + 1)
setLog((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
},
},
{
hotkey: 'Shift+R',
callback: (_e, { hotkey }) => {
setRedoCount((c) => c + 1)
setLog((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
},
},
])
return (
<div className="demo-section">
<h2>Basic Multi-Hotkey Registration</h2>
<p>
All three hotkeys are registered in a single <code>useHotkeys()</code>{' '}
call.
</p>
<div className="hotkey-grid">
<div>
<kbd>{formatForDisplay('Shift+S' as Hotkey)}</kbd> Save ({saveCount})
</div>
<div>
<kbd>{formatForDisplay('Shift+U' as Hotkey)}</kbd> Undo ({undoCount})
</div>
<div>
<kbd>{formatForDisplay('Shift+R' as Hotkey)}</kbd> Redo ({redoCount})
</div>
</div>
{log.length > 0 && (
<div className="log">
{log.map((entry, i) => (
<div key={i} className="log-entry">
{entry}
</div>
))}
</div>
)}
<pre className="code-block">{`useHotkeys([
{ hotkey: 'Shift+S', callback: () => save() },
{ hotkey: 'Shift+U', callback: () => undo() },
{ hotkey: 'Shift+R', callback: () => redo() },
])`}</pre>
</div>
)
}
// ---------------------------------------------------------------------------
// Common options with per-hotkey overrides
// ---------------------------------------------------------------------------
function CommonOptionsDemo() {
const [enabled, setEnabled] = React.useState(true)
const [counts, setCounts] = React.useState({ a: 0, b: 0, c: 0 })
useHotkeys(
[
{
hotkey: 'Alt+J',
callback: () => setCounts((c) => ({ ...c, a: c.a + 1 })),
},
{
hotkey: 'Alt+K',
callback: () => setCounts((c) => ({ ...c, b: c.b + 1 })),
},
{
hotkey: 'Alt+L',
callback: () => setCounts((c) => ({ ...c, c: c.c + 1 })),
options: { enabled: true },
},
],
{ enabled },
)
return (
<div className="demo-section">
<h2>Common Options with Per-Hotkey Overrides</h2>
<p>
<kbd>{formatForDisplay('Alt+J' as Hotkey)}</kbd> and{' '}
<kbd>{formatForDisplay('Alt+K' as Hotkey)}</kbd> respect the global
toggle. <kbd>{formatForDisplay('Alt+L' as Hotkey)}</kbd> overrides{' '}
<code>enabled: true</code> so it always works.
</p>
<div style={{ marginBottom: 12 }}>
<button onClick={() => setEnabled((e) => !e)}>
{enabled ? 'Disable' : 'Enable'} common hotkeys
</button>
</div>
<div className="hotkey-grid">
<div>
<kbd>{formatForDisplay('Alt+J' as Hotkey)}</kbd> Action A ({counts.a})
</div>
<div>
<kbd>{formatForDisplay('Alt+K' as Hotkey)}</kbd> Action B ({counts.b})
</div>
<div>
<kbd>{formatForDisplay('Alt+L' as Hotkey)}</kbd> Action C ({counts.c})
<span className="hint"> (always on)</span>
</div>
</div>
<pre className="code-block">{`useHotkeys(
[
{ hotkey: 'Alt+J', callback: () => actionA() },
{ hotkey: 'Alt+K', callback: () => actionB() },
{ hotkey: 'Alt+L', callback: () => actionC(),
options: { enabled: true } }, // overrides common
],
{ enabled }, // common option
)`}</pre>
</div>
)
}
// ---------------------------------------------------------------------------
// Dynamic hotkey list: add & remove at runtime
// ---------------------------------------------------------------------------
interface DynamicShortcut {
id: number
hotkey: string
label: string
count: number
}
let nextId = 0
const DEFAULT_SHORTCUTS: Array<DynamicShortcut> = [
{ id: nextId++, hotkey: 'Shift+A', label: 'Action A', count: 0 },
{ id: nextId++, hotkey: 'Shift+B', label: 'Action B', count: 0 },
{ id: nextId++, hotkey: 'Shift+C', label: 'Action C', count: 0 },
]
function DynamicHotkeysDemo() {
const [shortcuts, setShortcuts] =
React.useState<Array<DynamicShortcut>>(DEFAULT_SHORTCUTS)
const [newHotkey, setNewHotkey] = React.useState('')
const [newLabel, setNewLabel] = React.useState('')
const definitions: Array<UseHotkeyDefinition> = shortcuts.map((s) => ({
hotkey: s.hotkey as Hotkey,
callback: () => {
setShortcuts((prev) =>
prev.map((item) =>
item.id === s.id ? { ...item, count: item.count + 1 } : item,
),
)
},
}))
useHotkeys(definitions)
const addShortcut = () => {
const trimmed = newHotkey.trim()
if (!trimmed || !newLabel.trim()) return
setShortcuts((prev) => [
...prev,
{ id: nextId++, hotkey: trimmed, label: newLabel.trim(), count: 0 },
])
setNewHotkey('')
setNewLabel('')
}
const removeShortcut = (id: number) => {
setShortcuts((prev) => prev.filter((s) => s.id !== id))
}
return (
<div className="demo-section">
<h2>Dynamic Hotkey List</h2>
<p>
Add or remove hotkeys at runtime. Because <code>useHotkeys</code>{' '}
accepts a dynamic array, this is safe without breaking the rules of
hooks.
</p>
<div className="dynamic-list">
{shortcuts.map((s) => (
<div key={s.id} className="dynamic-item">
<kbd>{formatForDisplay(s.hotkey as Hotkey)}</kbd>
<span>{s.label}</span>
<span className="count">{s.count}</span>
<button onClick={() => removeShortcut(s.id)}>Remove</button>
</div>
))}
{shortcuts.length === 0 && (
<p className="hint">No shortcuts registered. Add one below.</p>
)}
</div>
<div className="add-form">
<input
type="text"
placeholder="Hotkey (e.g. Shift+D)"
value={newHotkey}
onChange={(e) => setNewHotkey(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') addShortcut()
}}
/>
<input
type="text"
placeholder="Label (e.g. Action D)"
value={newLabel}
onChange={(e) => setNewLabel(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') addShortcut()
}}
/>
<button onClick={addShortcut} disabled={!newHotkey || !newLabel}>
Add
</button>
</div>
<pre className="code-block">{`const shortcuts = useShortcutsConfig() // dynamic data
useHotkeys(
shortcuts.map((s) => ({
hotkey: s.key,
callback: s.action,
})),
)`}</pre>
</div>
)
}
// ---------------------------------------------------------------------------
// Mount
// ---------------------------------------------------------------------------
const rootElement = document.getElementById('root')!
ReactDOM.createRoot(rootElement).render(<App />)