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