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