UNPKG

@darksnow-ui/commander

Version:

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

769 lines (626 loc) 18.4 kB
# useCommand Hook - Guia Completo O `useCommand` é o hook fundamental para executar comandos existentes no Commander com controle total sobre configurações, estado e callbacks. ## 📋 Índice - [Visão Geral](#visão-geral) - [Assinaturas e Tipos](#assinaturas-e-tipos) - [Configurações](#configurações) - [Exemplos Práticos](#exemplos-práticos) - [Estado e Tracking](#estado-e-tracking) - [Tratamento de Erros](#tratamento-de-erros) - [Performance e Otimizações](#performance-e-otimizações) ## 🎯 Visão Geral O `useCommand` oferece múltiplas formas de uso, desde execução simples até controle completo com tracking de estado: ```tsx // 1. Execução simples const saveFile = useCommand("file:save"); await saveFile({ filename: "doc.txt" }); // 2. Com configurações const saveFile = useCommand("file:save", { throwOnError: false, timeout: 5000, }); // 3. Com tracking de estado completo const saveFile = useCommand("file:save", { trackState: true, }); console.log(saveFile.isLoading, saveFile.lastResult); // 4. Objeto completo de controle const saveFile = useCommand("file:save", { returnInvoker: true, }); ``` ## 🔧 Assinaturas e Tipos ### Assinaturas Disponíveis ```tsx // 1. Básico - retorna função invoke useCommand<TInput, TOutput>(key: CommandKey): (input?: TInput) => Promise<TOutput> // 2. Com opções - retorna função configurada useCommand<TInput, TOutput>( key: CommandKey, options: UseCommandOptions & { returnInvoker?: false } ): (input?: TInput) => Promise<TOutput> // 3. Objeto completo - returnInvoker: true useCommand<TInput, TOutput>( key: CommandKey, options: UseCommandOptions & { returnInvoker: true } ): CommandInvoker<TInput, TOutput> // 4. Com tracking - trackState: true useCommand<TInput, TOutput>( key: CommandKey, options: UseCommandOptions & { trackState: true } ): CommandInvoker<TInput, TOutput> ``` ### Interface CommandInvoker ```tsx interface CommandInvoker<TInput, TOutput> { // Estado do comando exists: boolean; isAvailable: boolean; isLoading: boolean; // Resultados e histórico lastResult: TOutput | null; lastError: Error | null; lastInput: TInput | null; lastExecution: Date | null; executionCount: number; // Comando completo command: Command<TInput, TOutput> | null; // Métodos de execução invoke: (input?: TInput) => Promise<TOutput>; attempt: (input?: TInput) => Promise<ExecutionResult<TOutput>>; execute: ( input?: TInput, callbacks?: ExecuteCallbacks, ) => Promise<TOutput | null>; // Verificações canInvoke: (input?: TInput) => Promise<boolean>; validateInput: (input?: TInput) => boolean | string; // Utilitários reset: () => void; clearError: () => void; refresh: () => void; } ``` ## ⚙️ Configurações ### UseCommandOptions Interface ```tsx interface UseCommandOptions<TInput, TOutput> { // Execução source?: "palette" | "shortcut" | "api"; // Default: 'api' throwOnError?: boolean; // Default: true timeout?: number; // Timeout em ms // Retry & Debounce retry?: number; // Tentativas (default: 0) retryDelay?: number | ((attempt: number) => number); debounce?: number; // Debounce em ms throttle?: number; // Throttle em ms // Input handling defaultInput?: Partial<TInput>; // Input padrão validateInput?: (input?: TInput) => boolean | string; transformInput?: (input?: TInput) => TInput; // Output handling transformOutput?: (output: TOutput) => TOutput; // Callbacks onSuccess?: (result: TOutput, input?: TInput) => void; onError?: (error: Error, input?: TInput) => void; onFinally?: (input?: TInput) => void; // Estado trackState?: boolean; // Ativa tracking returnInvoker?: boolean; // Retorna objeto completo resetOnKeyChange?: boolean; // Reset ao mudar key } ``` ## 📚 Exemplos Práticos ### 1. Execução Básica ```tsx function FileEditor() { // Forma mais simples const saveFile = useCommand<SaveInput, SaveResult>("file:save"); const handleSave = async () => { try { const result = await saveFile({ filename: "document.txt", content: editorContent, }); toast.success(`Salvo: ${result.filename}`); } catch (error) { toast.error("Erro ao salvar"); } }; return <button onClick={handleSave}>Save</button>; } ``` ### 2. Com Configurações Avançadas ```tsx function RobustUploader() { const uploadFile = useCommand("file:upload", { retry: 3, // 3 tentativas retryDelay: (attempt) => attempt * 1000, // Delay progressivo timeout: 30000, // 30s timeout throwOnError: false, // Não lança erro defaultInput: { quality: "high", compress: true, }, validateInput: (input) => { if (!input?.file) return "Arquivo obrigatório"; if (input.file.size > 10_000_000) return "Arquivo muito grande"; return true; }, transformInput: (input) => ({ ...input, uploadedAt: new Date(), userId: getCurrentUser().id, }), onSuccess: (result, input) => { analytics.track("file_uploaded", { size: input?.file.size, duration: result.duration, }); }, onError: (error, input) => { logger.error("Upload failed", { error, filename: input?.file.name }); }, }); const handleUpload = async (file: File) => { const result = await uploadFile({ file }); if (result) { setUploadedFiles((prev) => [...prev, result]); } }; } ``` ### 3. Com Tracking de Estado ```tsx function SmartFileUploader() { const uploader = useCommand("file:upload", { trackState: true, debounce: 500, // Evita uploads múltiplos onSuccess: (result) => { toast.success(`Upload concluído: ${result.filename}`); }, }); return ( <div> <input type="file" onChange={(e) => uploader.invoke({ file: e.target.files[0] })} disabled={uploader.isLoading || !uploader.isAvailable} /> {/* Estado visual */} {uploader.isLoading && ( <div className="flex items-center gap-2"> <Spinner /> <span>Enviando arquivo...</span> </div> )} {/* Histórico */} {uploader.lastResult && ( <div className="text-green-600"> ✅ Último upload: {uploader.lastResult.filename} <br /> 📊 Total de uploads: {uploader.executionCount} </div> )} {/* Erro */} {uploader.lastError && ( <div className="text-red-600"> ❌ Erro: {uploader.lastError.message} <button onClick={uploader.clearError}>Limpar</button> </div> )} {/* Controles */} <div className="flex gap-2 mt-4"> <button onClick={() => uploader.reset()} disabled={uploader.isLoading}> Reset Estado </button> <button onClick={uploader.refresh}>Verificar Disponibilidade</button> </div> </div> ); } ``` ### 4. Validação e Transformação ```tsx function UserProfileEditor() { const updateProfile = useCommand("user:update", { // Validação personalizada validateInput: (input) => { const errors = []; if (!input?.email?.includes("@")) errors.push("Email inválido"); if (!input?.name?.trim()) errors.push("Nome obrigatório"); return errors.length > 0 ? errors.join(", ") : true; }, // Transformação de input transformInput: (input) => ({ ...input, email: input.email?.toLowerCase(), name: input.name?.trim(), updatedAt: new Date().toISOString(), }), // Transformação de output transformOutput: (result) => ({ ...result, avatarUrl: result.avatar ? `/avatars/${result.avatar}` : null, }), trackState: true, }); const handleSubmit = async (formData) => { // A validação ocorre automaticamente const result = await updateProfile.invoke(formData); if (result) { // Output já foi transformado setUser(result); } }; } ``` ### 5. Execução com Callbacks Inline ```tsx function DocumentProcessor() { const processDoc = useCommand("document:process", { returnInvoker: true, timeout: 60000, // 1 minuto para processamento }); const handleProcess = async (document) => { // Usando execute com callbacks inline const result = await processDoc.execute( { documentId: document.id }, { onSuccess: (result) => { notification.success({ title: "Processamento Concluído", message: `Documento processado em ${result.duration}ms`, }); }, onError: (error) => { notification.error({ title: "Erro no Processamento", message: error.message, action: { label: "Tentar Novamente", onClick: () => handleProcess(document), }, }); }, onFinally: () => { analytics.track("document_process_attempt", { documentId: document.id, }); }, }, ); }; // Ou usando attempt para execução segura const handleSafeProcess = async (document) => { const result = await processDoc.attempt({ documentId: document.id }); if (result.success) { console.log("Sucesso:", result.result); } else { console.error("Erro:", result.error); } }; } ``` ## 📊 Estado e Tracking ### Estados Disponíveis ```tsx const command = useCommand("my:command", { trackState: true }); // Estados booleanos command.exists; // Comando existe no Commander command.isAvailable; // Comando disponível (when() = true) command.isLoading; // Execução em andamento // Resultados e histórico command.lastResult; // Último resultado bem-sucedido command.lastError; // Último erro ocorrido command.lastInput; // Último input utilizado command.lastExecution; // Data da última execução command.executionCount; // Número total de execuções // Comando original command.command; // Objeto Command completo ``` ### Ciclo de Vida dos Estados ```tsx // Estado inicial isLoading: false lastResult: null lastError: null executionCount: 0 // Durante execução invoke() → isLoading: true // Sucesso → isLoading: false → lastResult: [resultado] → lastError: null → executionCount: +1 → lastExecution: new Date() // Erro → isLoading: false → lastResult: [mantém anterior] → lastError: [erro] → executionCount: +1 → lastExecution: new Date() ``` ### Reset e Limpeza ```tsx // Reset completo command.reset(); // Limpa tudo, volta ao estado inicial // Limpeza seletiva command.clearError(); // Remove apenas lastError // Refresh command.refresh(); // Re-verifica isAvailable ``` ## 🚨 Tratamento de Erros ### Estratégias de Erro ```tsx // 1. Throw (padrão) const command = useCommand("risky:operation"); try { await command(); } catch (error) { // Trata erro } // 2. Return null const command = useCommand("risky:operation", { throwOnError: false, }); const result = await command(); // null se erro // 3. ExecutionResult const command = useCommand("risky:operation", { returnInvoker: true, }); const result = await command.attempt(); if (result.success) { // result.result } else { // result.error } ``` ### Retry Automático ```tsx const command = useCommand("unreliable:api", { retry: 3, retryDelay: (attempt) => { // Exponential backoff return Math.min(1000 * Math.pow(2, attempt), 10000); }, onError: (error, input) => { console.log(`Tentativa falhou: ${error.message}`); }, }); ``` ## ⚡ Performance e Otimizações ### Debounce e Throttle ```tsx // Debounce - aguarda pausa na entrada const searchCommand = useCommand("search:execute", { debounce: 300, // Aguarda 300ms sem nova execução transformInput: (input) => ({ ...input, timestamp: Date.now(), }), }); // Throttle - limita frequência const saveCommand = useCommand("auto:save", { throttle: 5000, // Máximo 1 save a cada 5s throwOnError: false, }); ``` ### Memoização e Re-renders ```tsx // ✅ Bom - não recria a cada render const command = useCommand("stable:command", { trackState: true, }); // ⚠️ Cuidado - recria options a cada render const command = useCommand("unstable:command", { onSuccess: (result) => { // Nova função a cada render console.log(result); }, }); // ✅ Melhor - callback memoizado const handleSuccess = useCallback((result) => { console.log(result); }, []); const command = useCommand("stable:command", { onSuccess: handleSuccess, }); ``` ### Reset Inteligente ```tsx const command = useCommand(dynamicKey, { trackState: true, resetOnKeyChange: true, // Reset automático quando key muda }); // Equivale a: useEffect(() => { command.reset(); }, [dynamicKey]); ``` ## 🔍 Verificações e Validações ### Verificação de Disponibilidade ```tsx const command = useCommand("conditional:command", { returnInvoker: true, }); // Verificação manual const canExecute = await command.canInvoke({ userId: 123 }); if (canExecute) { await command.invoke({ userId: 123 }); } // Verificação automática (padrão) // invoke() já verifica disponibilidade automaticamente ``` ### Validação Personalizada ```tsx const command = useCommand("user:create", { validateInput: (input) => { // Retorna true = válido // Retorna string = mensagem de erro // Retorna false = erro genérico if (!input?.email) return "Email é obrigatório"; if (!input?.password || input.password.length < 8) { return "Senha deve ter pelo menos 8 caracteres"; } return true; }, throwOnError: false, // Para capturar erros de validação }); ``` ## 📈 Casos de Uso Avançados ### 1. Command Chaining ```tsx function DocumentWorkflow() { const processDoc = useCommand("document:process", { trackState: true }); const generatePDF = useCommand("pdf:generate", { trackState: true }); const sendEmail = useCommand("email:send", { trackState: true }); const runWorkflow = async (document) => { try { // Processamento const processed = await processDoc.invoke({ document }); // Geração PDF const pdf = await generatePDF.invoke({ content: processed.content, }); // Envio por email await sendEmail.invoke({ to: document.author.email, attachment: pdf.url, }); toast.success("Workflow concluído!"); } catch (error) { toast.error(`Workflow falhou: ${error.message}`); } }; const isWorkflowRunning = processDoc.isLoading || generatePDF.isLoading || sendEmail.isLoading; return ( <div> <button onClick={() => runWorkflow(currentDocument)} disabled={isWorkflowRunning} > {isWorkflowRunning ? "Processando..." : "Executar Workflow"} </button> {/* Status de cada etapa */} <div className="mt-4 space-y-2"> <WorkflowStep label="Processamento" status={getStepStatus(processDoc)} /> <WorkflowStep label="Geração PDF" status={getStepStatus(generatePDF)} /> <WorkflowStep label="Envio Email" status={getStepStatus(sendEmail)} /> </div> </div> ); } ``` ### 2. Conditional Execution ```tsx function SmartSaveButton() { const autoSave = useCommand("document:auto-save", { trackState: true, throttle: 10000, // Auto-save máximo a cada 10s throwOnError: false, }); const manualSave = useCommand("document:save", { trackState: true, onSuccess: () => toast.success("Documento salvo!"), }); // Auto-save quando conteúdo muda useEffect(() => { if (documentContent && hasUnsavedChanges) { autoSave.invoke({ content: documentContent }); } }, [documentContent]); // Save manual const handleManualSave = async () => { const result = await manualSave.invoke({ content: documentContent, force: true, // Força save mesmo se auto-save recente }); }; const lastSaveTime = Math.max( autoSave.lastExecution?.getTime() || 0, manualSave.lastExecution?.getTime() || 0, ); return ( <div> <button onClick={handleManualSave} disabled={manualSave.isLoading}> {manualSave.isLoading ? "Salvando..." : "Salvar"} </button> <div className="text-sm text-gray-500"> {lastSaveTime && ( <span>Último save: {formatDistanceToNow(lastSaveTime)}</span> )} {autoSave.isLoading && ( <span className="ml-2">• Auto-save em andamento</span> )} </div> </div> ); } ``` ## 🎯 Boas Práticas ### 1. Tipagem Consistente ```tsx // ✅ Defina interfaces claras interface SaveDocumentInput { content: string; title: string; tags?: string[]; } interface SaveDocumentOutput { id: string; url: string; savedAt: Date; } const saveDoc = useCommand<SaveDocumentInput, SaveDocumentOutput>("doc:save"); ``` ### 2. Error Boundaries ```tsx function DocumentEditor() { const saveDoc = useCommand("doc:save", { onError: (error, input) => { // Log para monitoramento logger.error("Save failed", { error: error.message, documentId: input?.id, }); // Fallback local localStorage.setItem("unsaved_doc", JSON.stringify(input)); }, }); } ``` ### 3. Loading States Granulares ```tsx function MultiStepForm() { const validateStep = useCommand("form:validate-step", { trackState: true }); const submitForm = useCommand("form:submit", { trackState: true }); const isValidating = validateStep.isLoading; const isSubmitting = submitForm.isLoading; const isProcessing = isValidating || isSubmitting; return ( <form> {/* Campos do formulário */} <button type="button" onClick={() => validateStep.invoke({ step: currentStep })} disabled={isProcessing} > {isValidating ? "Validando..." : "Validar Etapa"} </button> <button type="submit" onClick={() => submitForm.invoke(formData)} disabled={isProcessing || !isFormValid} > {isSubmitting ? "Enviando..." : "Enviar"} </button> </form> ); } ``` O `useCommand` é o hook mais poderoso e flexível do Commander, oferecendo controle total sobre execução, estado e comportamento dos comandos.