@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
781 lines (620 loc) • 15.7 kB
Markdown
# useCustomCommand - Guia Completo
> 🎯 **O hook principal para criar comandos temporários em componentes React**
## 📋 Índice
1. [Conceito](#conceito)
2. [Anatomia do Hook](#anatomia-do-hook)
3. [Ciclo de Vida](#ciclo-de-vida)
4. [Sistema de Disponibilidade](#sistema-de-disponibilidade)
5. [Retorno do Hook (Invoker)](#retorno-do-hook-invoker)
6. [Hooks Especializados](#hooks-especializados)
7. [Padrões Avançados](#padrões-avançados)
8. [Performance e Otimização](#performance-e-otimização)
9. [Troubleshooting](#troubleshooting)
---
## Conceito
O `useCustomCommand` é um React Hook que permite registrar comandos temporários que:
- ✅ **Existem apenas enquanto o componente está montado**
- ✅ **Aparecem automaticamente no Command Palette**
- ✅ **São removidos quando o componente desmonta**
- ✅ **Mantêm type safety com TypeScript**
- ✅ **Integram com o estado do componente**
### Por que usar?
```tsx
// ❌ Sem useCustomCommand - ações espalhadas
function Modal({ onSave, onClose }) {
return (
<div>
<button onClick={onSave}>Salvar (Ctrl+S)</button>
<button onClick={onClose}>Fechar (ESC)</button>
</div>
)
}
// ✅ Com useCustomCommand - ações centralizadas e descobríveis
function Modal({ onSave, onClose }) {
useCustomCommand({
key: 'modal:save',
label: 'Salvar',
shortcut: 'ctrl+s',
handle: async () => onSave()
})
useCustomCommand({
key: 'modal:close',
label: 'Fechar',
shortcut: 'escape',
handle: async () => onClose()
})
// Comandos disponíveis via Ctrl+Shift+P
return <div>{/* ... */}</div>
}
```
---
## Anatomia do Hook
### Interface TypeScript
```typescript
interface UseCustomCommandProps<TInput = any, TOutput = any> {
// Identificador único do comando
key: string
// Label exibido no Command Palette
label?: string
// Descrição detalhada
description?: string
// Categoria para organização
category?: CommandCategory
// Tags para busca
tags?: string[]
// Ícone visual (emoji ou componente)
icon?: string
// Atalho de teclado
shortcut?: string
// Função que determina disponibilidade
when?: () => boolean | Promise<boolean>
// Handler assíncrono do comando
handle: (input?: TInput) => Promise<TOutput>
// Timeout em ms (padrão: 30000)
timeout?: number
// Prioridade na busca (maior = primeiro)
priority?: number
// Proprietário do comando
owner?: string
// Palavras-chave adicionais para busca
searchKeywords?: string[]
}
```
### Uso Básico
```tsx
function MyComponent() {
const [data, setData] = useState(null)
const saveCommand = useCustomCommand({
key: 'my:save',
label: 'Salvar Dados',
handle: async () => {
const result = await api.save(data)
return result
}
})
return (
<button onClick={() => saveCommand.invoke()}>
Salvar
</button>
)
}
```
---
## Ciclo de Vida
### 1. **Registro (Mount)**
Quando o componente monta, o comando é registrado automaticamente:
```tsx
function FileEditor({ fileId }) {
// Comando registrado quando FileEditor monta
useCustomCommand({
key: `file:save:${fileId}`,
label: 'Salvar Arquivo',
handle: async () => saveFile(fileId)
})
// Se fileId mudar, um novo comando é registrado
// e o anterior é removido
}
```
### 2. **Atualização (Update)**
O comando é re-registrado quando suas dependências mudam:
```tsx
function DynamicCommand() {
const [mode, setMode] = useState('view')
// Re-registra quando 'mode' muda
useCustomCommand({
key: 'action',
label: mode === 'edit' ? 'Salvar' : 'Editar',
handle: async () => {
if (mode === 'edit') {
await save()
setMode('view')
} else {
setMode('edit')
}
}
})
}
```
### 3. **Remoção (Unmount)**
O comando é removido automaticamente quando o componente desmonta:
```tsx
function TemporaryFeature({ isVisible }) {
if (!isVisible) return null
// Comando existe apenas quando isVisible = true
useCustomCommand({
key: 'temp:action',
label: 'Ação Temporária',
handle: async () => doSomething()
})
return <div>Feature Temporária</div>
}
```
---
## Sistema de Disponibilidade
### A função `when`
A função `when` determina se o comando está disponível:
```tsx
useCustomCommand({
key: 'save',
label: 'Salvar',
when: () => hasChanges && !isSaving,
handle: async () => save()
})
```
### Quando é avaliada
1. **Durante busca** - Para filtrar comandos indisponíveis
2. **Antes da execução** - Para validar se ainda pode executar
3. **Na UI** - Para mostrar estado desabilitado
### Importante: Não é reativa!
```tsx
// ❌ ERRADO - Pensa que vai atualizar automaticamente
function BadExample() {
const [canSave, setCanSave] = useState(false)
useCustomCommand({
key: 'save',
when: () => canSave, // Não re-avalia sozinho!
handle: async () => save()
})
}
// ✅ CORRETO - Re-registra quando estado muda
function GoodExample() {
const [canSave, setCanSave] = useState(false)
// Re-registra o comando quando canSave muda
const command = useMemo(() => ({
key: 'save',
label: canSave ? 'Salvar' : 'Salvar (desabilitado)',
when: () => canSave,
handle: async () => save()
}), [canSave])
useCustomCommand(command)
}
```
### Padrões de disponibilidade
```tsx
// Baseado em permissões
when: () => user.role === 'admin'
// Baseado em estado
when: () => form.isValid && !form.isSubmitting
// Baseado em contexto
when: () => selectedItems.length > 0
// Assíncrono (use com cuidado!)
when: async () => {
const hasPermission = await checkPermission()
return hasPermission
}
```
---
## Retorno do Hook (Invoker)
O hook retorna um objeto `invoker` com métodos úteis:
```tsx
interface CustomCommandInvoker<TInput, TOutput> {
// Chave do comando
key: CommandKey
// Verifica se comando existe
exists: () => boolean
// Verifica disponibilidade
isAvailable: () => Promise<boolean>
// Executa o comando
invoke: (input?: TInput, source?: string) => Promise<TOutput>
// Executa com tratamento de erro
attempt: (input?: TInput, source?: string) => Promise<ExecutionResult<TOutput>>
// Obtém o comando registrado
getCommand: () => Command<TInput, TOutput> | undefined
}
```
### Exemplos de uso
```tsx
function MyComponent() {
const saveCommand = useCustomCommand({
key: 'save',
label: 'Salvar',
handle: async (data: SaveData) => {
return await api.save(data)
}
})
// Executar diretamente
const handleSave = async () => {
const result = await saveCommand.invoke({ title: 'Teste' })
console.log('Salvo:', result)
}
// Executar com tratamento de erro
const handleSafeSave = async () => {
const result = await saveCommand.attempt({ title: 'Teste' })
if (result.success) {
console.log('Sucesso:', result.result)
} else {
console.error('Erro:', result.error)
}
}
// Verificar disponibilidade
const checkCommand = async () => {
if (await saveCommand.isAvailable()) {
console.log('Comando disponível')
}
}
// Acessar comando completo
const command = saveCommand.getCommand()
console.log('Atalho:', command?.shortcut)
}
```
---
## Hooks Especializados
### useAction
Para ações simples sem input/output:
```tsx
function Toolbar() {
const refresh = useAction(
'refresh',
'Atualizar',
() => {
window.location.reload()
},
{
icon: '🔄',
shortcut: 'f5'
}
)
return <button onClick={() => refresh.invoke()}>Refresh</button>
}
```
### useToggleCommand
Para comandos toggle (on/off):
```tsx
function Settings() {
const [darkMode, setDarkMode] = useState(false)
const toggleDark = useToggleCommand(
'dark-mode',
'Modo Escuro',
darkMode,
setDarkMode,
{
icon: darkMode ? '🌙' : '☀️',
shortcut: 'ctrl+shift+d'
}
)
// Label automático: "Disable Modo Escuro" / "Enable Modo Escuro"
}
```
### useModalCommand
Para comandos que abrem modais:
```tsx
function UserList() {
const createUser = useModalCommand(
'user:create',
'Novo Usuário',
async () => {
const userData = await showCreateUserModal()
if (userData) {
return await api.createUser(userData)
}
},
{
icon: '👤',
shortcut: 'ctrl+n'
}
)
}
```
### useNavigationCommand
Para navegação:
```tsx
function ProductPage() {
const router = useRouter()
useNavigationCommand(
'go:home',
'Ir para Home',
'/',
router.push,
{
icon: '🏠',
shortcut: 'alt+h'
}
)
}
```
### useContextualCommands
Para listas dinâmicas:
```tsx
function TodoList({ todos }) {
// Cria um comando para cada todo
useContextualCommands(
todos,
(todo) => ({
key: `todo:complete:${todo.id}`,
label: `Completar: ${todo.title}`,
icon: '✅',
handle: async () => completeTodo(todo.id)
})
)
}
```
---
## Padrões Avançados
### 1. **Comandos com Estado**
```tsx
function StatefulCommand() {
const [lastExecution, setLastExecution] = useState(null)
useCustomCommand({
key: 'stateful',
label: lastExecution
? `Última execução: ${lastExecution}`
: 'Executar',
handle: async () => {
const now = new Date().toLocaleTimeString()
setLastExecution(now)
return { executedAt: now }
}
})
}
```
### 2. **Comandos Compostos**
```tsx
function BulkActions({ items }) {
const [processing, setProcessing] = useState(false)
useCustomCommand({
key: 'bulk:process',
label: `Processar ${items.length} itens`,
when: () => items.length > 0 && !processing,
handle: async () => {
setProcessing(true)
const results = []
for (const item of items) {
results.push(await processItem(item))
}
setProcessing(false)
return { processed: results.length }
}
})
}
```
### 3. **Comandos com Confirmação**
```tsx
function DangerousAction() {
const deleteCommand = useCustomCommand({
key: 'delete:all',
label: 'Deletar Tudo',
icon: '⚠️',
category: 'danger',
priority: -10, // Baixa prioridade
handle: async () => {
const confirmed = await showConfirmDialog({
title: 'Deletar Tudo?',
message: 'Esta ação não pode ser desfeita.',
confirmText: 'Deletar',
danger: true
})
if (confirmed) {
await api.deleteAll()
return { deleted: true }
}
return { deleted: false }
}
})
}
```
### 4. **Comandos com Loading State**
```tsx
function AsyncCommand() {
const [isLoading, setIsLoading] = useState(false)
const command = useCustomCommand({
key: 'async:operation',
label: isLoading ? 'Processando...' : 'Iniciar Processo',
when: () => !isLoading,
handle: async () => {
setIsLoading(true)
try {
const result = await longRunningOperation()
return result
} finally {
setIsLoading(false)
}
}
})
}
```
### 5. **Comandos com Progresso**
```tsx
function ProgressCommand() {
const [progress, setProgress] = useState(0)
useCustomCommand({
key: 'upload',
label: progress > 0 ? `Upload: ${progress}%` : 'Fazer Upload',
when: () => progress === 0,
handle: async () => {
const file = await selectFile()
return uploadWithProgress(file, (percent) => {
setProgress(percent)
}).finally(() => {
setProgress(0)
})
}
})
}
```
---
## Performance e Otimização
### 1. **Memoização de Comandos**
```tsx
// ❌ Re-cria comando a cada render
function BadPerformance() {
useCustomCommand({
key: 'action',
label: 'Action',
handle: async () => {
// Complex logic
}
})
}
// ✅ Memoiza comando pesado
function GoodPerformance() {
const command = useMemo(() => ({
key: 'action',
label: 'Action',
handle: async () => {
// Complex logic
}
}), [/* dependências */])
useCustomCommand(command)
}
```
### 2. **Callbacks Estáveis**
```tsx
function StableCallbacks() {
const [data, setData] = useState(null)
// ✅ useCallback para handler estável
const handleSave = useCallback(async () => {
return await api.save(data)
}, [data])
useCustomCommand({
key: 'save',
label: 'Save',
handle: handleSave
})
}
```
### 3. **Evitar Re-registros**
```tsx
// ❌ Re-registra a cada mudança de count
function TooManyRegistrations({ count }) {
useCustomCommand({
key: 'show-count',
label: `Count: ${count}`,
handle: async () => alert(count)
})
}
// ✅ Usa closure para acessar valor atual
function OptimizedRegistration({ count }) {
const countRef = useRef(count)
countRef.current = count
const command = useMemo(() => ({
key: 'show-count',
label: 'Show Count',
handle: async () => alert(countRef.current)
}), []) // Sem dependências!
useCustomCommand(command)
}
```
### 4. **Batch de Comandos**
```tsx
// ❌ Muitas chamadas individuais
function ManyCommands({ items }) {
items.forEach(item => {
useCustomCommand({
key: `item:${item.id}`,
label: item.name,
handle: async () => processItem(item)
})
})
}
// ✅ Use useContextualCommands
function OptimizedCommands({ items }) {
useContextualCommands(items, (item) => ({
key: `item:${item.id}`,
label: item.name,
handle: async () => processItem(item)
}))
}
```
---
## Troubleshooting
### Comando não aparece no Command Palette
```tsx
// Verifique:
// 1. Key única
useCustomCommand({
key: `unique:key:${id}`, // ✅ Única por instância
// ...
})
// 2. Label definido
useCustomCommand({
key: 'test',
label: 'Test Command', // ✅ Obrigatório
// ...
})
// 3. Função when retornando true
useCustomCommand({
key: 'test',
when: () => {
console.log('Checking availability') // Debug
return true
},
// ...
})
```
### Comando não executa
```tsx
// Verifique erros no console
useCustomCommand({
key: 'test',
handle: async () => {
try {
// Sua lógica
} catch (error) {
console.error('Command error:', error)
throw error // Re-throw para o Commander capturar
}
}
})
```
### Memory leaks
```tsx
// ❌ Criar listeners sem cleanup
useCustomCommand({
key: 'listen',
handle: async () => {
window.addEventListener('resize', handler) // Leak!
}
})
// ✅ Cleanup adequado
useEffect(() => {
const handler = () => {}
useCustomCommand({
key: 'listen',
handle: async () => {
// Use o handler
}
})
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
```
### Performance ruim
```tsx
// Use React DevTools Profiler para identificar:
// 1. Re-renders excessivos
// 2. Comandos sendo re-registrados
// 3. Handlers pesados sem memoização
// Habilite logs de debug:
if (process.env.NODE_ENV === 'development') {
console.log('Registering command:', key)
}
```
---
## 🎯 Resumo
- **useCustomCommand** cria comandos temporários vinculados ao ciclo de vida do componente
- **when** não é reativo - re-registre o comando quando o estado mudar
- **Invoker** retornado permite execução programática
- **Hooks especializados** simplificam casos comuns
- **Memoização** é importante para performance
- **IDs únicos** previnem conflitos entre instâncias
O hook transforma componentes React em interfaces poderosas e acessíveis, onde cada ação importante pode ser descoberta e executada via Command Palette.