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