UNPKG

@darksnow-ui/commander

Version:

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

781 lines (638 loc) 18.9 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.