useShortcut: a React keyboard shortcut hook
Registers a shortcut for the lifetime of the calling component. Automatically adds on mount and removes on unmount.
import { useShortcut } from '@tactile-js/react';
function Editor() { useShortcut({ id: 'palette.open', keys: 'mod+k', when: "scope == 'editor'", group: 'Navigation', description: 'Open command palette', priority: 0, handler: () => setOpen(true), });}Signature
Section titled “Signature”function useShortcut(rule: KeybindingRule, deps?: unknown[]): voidAccepts any KeybindingRule field.
Stable handler (no stale closures)
Section titled “Stable handler (no stale closures)”The handler is read through a ref, so passing a fresh closure every render does not re-register the binding:
function Editor() { const [count, setCount] = useState(0);
useShortcut({ id: 'increment', keys: 'mod+i', handler: () => setCount((c) => c + 1), // always sees latest setCount });
return <span>{count}</span>;}No dependency-array juggling for the handler itself.
When re-registration happens
Section titled “When re-registration happens”The binding re-registers only when these change:
rule.idrule.keysrule.whenrule.eventTyperule.match- Items in the optional
depsarray
// Re-register when mode changesuseShortcut( { id: 'action', keys: mode === 'vim' ? 'j' : 'arrowdown', handler: moveDown, }, [mode],);Full rule example
Section titled “Full rule example”useShortcut({ id: 'file.save', keys: ['mod+s', 'ctrl+s'], when: "!readOnly && editorFocus", group: 'File', description: 'Save document', priority: 5, match: 'hybrid', preventDefault: true, eventType: 'keydown', handler: (event, info) => { console.log(info.id, info.keys, info.sequence); save(); // return false to skip preventDefault },});when gating
Section titled “when gating”Pair with useScope or imperative context:
const { setScope } = useScope();
useShortcut({ id: 'editor.bold', keys: 'mod+b', when: "scope == 'editor'", handler: () => toggleBold(),});
<div tabIndex={0} onFocus={() => setScope('editor')} onBlur={() => setScope('global')}/>Sequences
Section titled “Sequences”useShortcut({ id: 'go.inbox', keys: 'g i', description: 'Go to inbox', handler: () => navigate('/inbox'),});Collision override
Section titled “Collision override”// Registered later + higher priority winsuseShortcut({ id: 'default', keys: 'mod+k', handler: defaultAction });useShortcut({ id: 'override', keys: 'mod+k', priority: 10, handler: overrideAction });Conditional registration
Section titled “Conditional registration”Don’t conditionally call the hook — register always and gate with when:
// ✅ GooduseShortcut({ id: 'feature', keys: 'mod+f', when: 'featureEnabled', handler: runFeature,});
// ❌ Bad — breaks rules of hooksif (featureEnabled) { useShortcut({ ... });}Sync context instead:
useEffect(() => { if (featureEnabled) engine.context.set('featureEnabled', true); else engine.context.delete('featureEnabled');}, [featureEnabled]);Or use deps to change the when string:
useShortcut( { id: 'feature', keys: 'mod+f', when: featureEnabled ? 'alwaysTrue' : 'alwaysFalse', // prefer real context keys handler: runFeature, }, [featureEnabled],);Better pattern with context:
const { set } = useScope();useEffect(() => set('featureEnabled', featureEnabled), [featureEnabled]);useShortcut({ id: 'feature', keys: 'mod+f', when: 'featureEnabled', handler: runFeature });Next steps
Section titled “Next steps”- useScope — drive the
whencontext from components - Key syntax — everything
keysaccepts - Playground — see these rules running live