Skip to content

useCommandPalette

What this solves

Command palettes require a consistent model for:

  • Open/close state
  • A stable list of commands (from multiple features)
  • Grouping and search metadata
  • Safe execution and cleanup
  • A default keyboard trigger (Cmd/Ctrl+K)

useCommandPalette provides a small, composable API for registering and executing commands while keeping the UI rendering separate.


When to use it

Use useCommandPalette when you want:

  • A keyboard-driven command surface (navigation + actions)
  • A central registry of feature commands
  • Composition with keyboard shortcuts (open palette, execute commands)

When NOT to use it

Do not use useCommandPalette when:

  • You only need a small dropdown/menu for a single screen
  • Commands must be persisted remotely or managed as server state
  • You need advanced ranking, fuzzy search tuning, or user-customizable commands (this hook focuses on state and registration)

Basic usage

The hook provides state/handlers; the UI is typically rendered with CommandPalette.

tsx
import React, { useMemo } from 'react'
import { useCommandPalette, type Command } from '@/shared/hooks'
import { CommandPalette } from '@/shared/ui/components/CommandPalette'

export function Example() {
  const commands: Command[] = useMemo(
    () => [
      { id: 'go-dashboard', label: 'Go to Dashboard', group: 'Navigation', action: () => {/* navigate('/dashboard') */} },
      { id: 'open-settings', label: 'Open Settings', group: 'Actions', action: () => {/* open settings */} },
    ],
    []
  )

  const palette = useCommandPalette({ initialCommands: commands })

  return (
    <CommandPalette
      open={palette.isOpen}
      onOpenChange={(open) => (open ? palette.open() : palette.close())}
      commands={palette.commands}
      groupedCommands={palette.groupedCommands}
      onSelect={(cmd) => palette.execute(cmd.id)}
    />
  )
}

Common patterns

Pattern 1 — Feature-level command registration with cleanup

Use add()/remove() in an effect so feature routes can register commands on mount and clean up on unmount.

tsx
import React from 'react'
import { useCommandPalette, type Command } from '@/shared/hooks'

export function UsersFeature() {
  const palette = useCommandPalette()

  React.useEffect(() => {
    const cmds: Command[] = [
      { id: 'users-create', label: 'Create User', group: 'Users', action: async () => {/* open create drawer */} },
      { id: 'users-invite', label: 'Invite User', group: 'Users', action: async () => {/* open invite flow */} },
    ]

    palette.add(cmds)

    return () => {
      palette.remove(cmds.map((c) => c.id))
    }
  }, [palette])

  return <div />
}

Why this is recommended:

  • Commands stay aligned with route/module lifecycle
  • Avoids a single massive global command list

Pattern 2 — Compose with additional shortcuts / workflows

useCommandPalette already registers Cmd/Ctrl+K to toggle by default. You can add other triggers or command-specific shortcuts.

tsx
import { useCommandPalette, useKeyboardShortcut } from '@/shared/hooks'

export function PaletteTriggers() {
  const palette = useCommandPalette()

  useKeyboardShortcut({ key: '/' }, () => palette.open(), { ignoreInputs: false })

  return <button type="button" onClick={palette.open}>Open</button>
}

Things to avoid

  • Using unstable command IDs (IDs should be stable and unique)
  • Calling register() from multiple features (it replaces the entire list; prefer add()/remove() for composition)
  • Assuming execute() will run disabled commands (it explicitly skips disabled: true)


API reference (concise)

ts
export interface Command {
  id: string
  label: string
  keywords?: string[]
  icon?: React.ReactNode
  group?: string
  shortcut?: string
  disabled?: boolean
  action: () => void | Promise<void>
}

export interface UseCommandPaletteOptions {
  initialCommands?: Command[]
  enableShortcut?: boolean
  onExecute?: (command: Command) => void
}

export interface UseCommandPaletteReturn {
  isOpen: boolean
  commands: Command[]
  groupedCommands: Map<string, Command[]>
  open: () => void
  close: () => void
  toggle: () => void
  register: (commands: Command[]) => void
  add: (commands: Command[]) => void
  remove: (ids: string[]) => void
  clear: () => void
  execute: (id: string) => Promise<void>
}

export function useCommandPalette(options?: UseCommandPaletteOptions): UseCommandPaletteReturn

Final notes

  • Executing a command closes the palette.
  • Keep command lists memoized where possible to avoid unnecessary churn, especially when using register().
  • If a command is async, execute() will await it before closing.