Appearance
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; preferadd()/remove()for composition) - Assuming
execute()will run disabled commands (it explicitly skipsdisabled: true)
Related primitives
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): UseCommandPaletteReturnFinal 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.