Skip to main content

React Bindings

Provider, hooks, and context for using duck-vim in React applications.

import {
  KeyProvider,
  KeyContext,
  useKeyCommands,
  useKeyBind,
  useKeySequence,
  useKeyRecorder,
} from '@gentleduck/vim/react'
import {
  KeyProvider,
  KeyContext,
  useKeyCommands,
  useKeyBind,
  useKeySequence,
  useKeyRecorder,
} from '@gentleduck/vim/react'

Types

KeyContextValue

The shape of the value provided by KeyContext.

interface KeyContextValue {
  registry: Registry
  handler: KeyHandler
  sequenceManager: SequenceManager
  timeoutMs: number
  defaultOptions?: Partial<KeyBindOptions>
}
interface KeyContextValue {
  registry: Registry
  handler: KeyHandler
  sequenceManager: SequenceManager
  timeoutMs: number
  defaultOptions?: Partial<KeyBindOptions>
}

KeyBindHookOptions

Options for useKeyBind. Extends KeyBindOptions with a targetRef.

interface KeyBindHookOptions extends Partial<KeyBindOptions> {
  targetRef?: React.RefObject<HTMLElement | null>
}
interface KeyBindHookOptions extends Partial<KeyBindOptions> {
  targetRef?: React.RefObject<HTMLElement | null>
}

SequenceHookOptions

Options for useKeySequence. Extends SequenceOptions with a targetRef.

interface SequenceHookOptions extends SequenceOptions {
  targetRef?: React.RefObject<HTMLElement | null>
}
interface SequenceHookOptions extends SequenceOptions {
  targetRef?: React.RefObject<HTMLElement | null>
}

KeyRecorderReturn

interface KeyRecorderReturn {
  state: KeyRecorderState
  start: (target?: HTMLElement) => void
  stop: () => void
  reset: () => void
}
interface KeyRecorderReturn {
  state: KeyRecorderState
  start: (target?: HTMLElement) => void
  stop: () => void
  reset: () => void
}

KeyProvider

Wraps your app (or a subtree) with the keyboard command system. Creates a Registry, KeyHandler, and SequenceManager, attaches the handler on mount, and cleans up on unmount.

interface KeyProviderProps {
  debug?: boolean                      // Enable debug logging
  timeoutMs?: number                   // Sequence timeout (default: 600)
  defaultOptions?: Partial<KeyBindOptions>  // Default options for all bindings
  children: React.ReactNode
}
interface KeyProviderProps {
  debug?: boolean                      // Enable debug logging
  timeoutMs?: number                   // Sequence timeout (default: 600)
  defaultOptions?: Partial<KeyBindOptions>  // Default options for all bindings
  children: React.ReactNode
}

Example:

<KeyProvider debug={process.env.NODE_ENV === 'development'} timeoutMs={600}>
  <App />
</KeyProvider>
<KeyProvider debug={process.env.NODE_ENV === 'development'} timeoutMs={600}>
  <App />
</KeyProvider>

KeyContext

Direct access to the context value. Use this for advanced scenarios where the hooks do not cover your needs.

const ctx = React.useContext(KeyContext)
// ctx.registry, ctx.handler, ctx.sequenceManager
const ctx = React.useContext(KeyContext)
// ctx.registry, ctx.handler, ctx.sequenceManager

useKeyCommands(commands, options?)

Register multiple bindings at once using a record of key sequences to commands.

function useKeyCommands(
  commands: Record<string, Command>,
  options?: KeyBindOptions,
): void
function useKeyCommands(
  commands: Record<string, Command>,
  options?: KeyBindOptions,
): void

Bindings are registered on mount and cleaned up on unmount. If the commands object changes, old bindings are unregistered and new ones are registered.

Example:

function App() {
  const navigate = useNavigate()
 
  useKeyCommands({
    'g+d': {
      name: 'Go to Dashboard',
      execute: () => navigate('/dashboard'),
    },
    'g+s': {
      name: 'Go to Settings',
      execute: () => navigate('/settings'),
    },
    'ctrl+k': {
      name: 'Command Palette',
      execute: () => setOpen(true),
    },
  }, { ignoreInputs: true })
 
  return <div>...</div>
}
function App() {
  const navigate = useNavigate()
 
  useKeyCommands({
    'g+d': {
      name: 'Go to Dashboard',
      execute: () => navigate('/dashboard'),
    },
    'g+s': {
      name: 'Go to Settings',
      execute: () => navigate('/settings'),
    },
    'ctrl+k': {
      name: 'Command Palette',
      execute: () => setOpen(true),
    },
  }, { ignoreInputs: true })
 
  return <div>...</div>
}

useKeyBind(binding, handler, options?)

Register a single key binding.

function useKeyBind(
  binding: string,
  handler: () => void,
  options?: KeyBindHookOptions,
): void
function useKeyBind(
  binding: string,
  handler: () => void,
  options?: KeyBindHookOptions,
): void

The handler function is captured in a ref internally, so it always calls the latest version without needing it in the dependency array.

useKeyBind('ctrl+k', () => setOpen(true), { preventDefault: true })
useKeyBind('ctrl+k', () => setOpen(true), { preventDefault: true })

useKeySequence(steps, handler, options?)

Register a multi-key sequence.

function useKeySequence(
  steps: string[],
  handler: () => void,
  options?: SequenceHookOptions,
): void
function useKeySequence(
  steps: string[],
  handler: () => void,
  options?: SequenceHookOptions,
): void

Example:

// Press g, then d (within timeout)
useKeySequence(['g', 'd'], () => navigate('/dashboard'))
 
// Press Ctrl+K, then Ctrl+D
useKeySequence(['ctrl+k', 'ctrl+d'], () => deleteAll())
 
// Scoped to an element
useKeySequence(['j', 'k'], () => scrollUp(), {
  targetRef: listRef,
  timeout: 400,
})
// Press g, then d (within timeout)
useKeySequence(['g', 'd'], () => navigate('/dashboard'))
 
// Press Ctrl+K, then Ctrl+D
useKeySequence(['ctrl+k', 'ctrl+d'], () => deleteAll())
 
// Scoped to an element
useKeySequence(['j', 'k'], () => scrollUp(), {
  targetRef: listRef,
  timeout: 400,
})

useKeyRecorder()

React wrapper around KeyRecorder for building shortcut customization UIs.

function useKeyRecorder(): KeyRecorderReturn
function useKeyRecorder(): KeyRecorderReturn

Example:

function ShortcutRecorder() {
  const { state, start, stop, reset } = useKeyRecorder()
 
  return (
    <div>
      <button onClick={() => start()}>
        {state.isRecording ? 'Press a key combination...' : 'Click to record'}
      </button>
 
      {state.recorded && (
        <div>
          <span>Recorded: {state.recorded}</span>
          <button onClick={reset}>Clear</button>
        </div>
      )}
 
      {state.isRecording && (
        <button onClick={stop}>Cancel</button>
      )}
    </div>
  )
}
function ShortcutRecorder() {
  const { state, start, stop, reset } = useKeyRecorder()
 
  return (
    <div>
      <button onClick={() => start()}>
        {state.isRecording ? 'Press a key combination...' : 'Click to record'}
      </button>
 
      {state.recorded && (
        <div>
          <span>Recorded: {state.recorded}</span>
          <button onClick={reset}>Clear</button>
        </div>
      )}
 
      {state.isRecording && (
        <button onClick={stop}>Cancel</button>
      )}
    </div>
  )
}

Hook comparison

HookUse caseSupports sequences
useKeyBindSingle bindingNo
useKeySequenceMulti-step sequenceYes
useKeyCommandsBulk registrationVia Registry (e.g. 'g+d')
useKeyRecorderSettings UI--