UNPKG

@darksnow-ui/commander

Version:

Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs

781 lines (619 loc) 15.8 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.