UNPKG

@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
# 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.