@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
1,631 lines (1,332 loc) • 41.5 kB
Markdown
# useInvoker Hook - Guia Completo
O `useInvoker` é um hook que retorna **diretamente a função de execução** de um comando, eliminando a necessidade de chamar `.invoke()`. É a forma mais simples e direta de executar comandos no Commander.
## 📋 Índice
- [Visão Geral](#visão-geral)
- [Hook Principal](#hook-principal---useinvoker)
- [Hooks Especializados](#hooks-especializados)
- [useAction](#1-useaction---para-comandos-sem-parâmetros)
- [useCommandState](#2-usecommandstate---com-estado-completo)
- [useSafeInvoker](#3-usesafeinvoker---execução-segura)
- [useBoundInvoker](#4-useboundinvoker---input-pré-configurado)
- [useToggleInvoker](#5-usetoggleinvoker---comandos-toggle)
- [useBatchInvoker](#6-usebatchinvoker---execução-sequencial)
- [useParallelInvoker](#7-useparallelinvoker---execução-paralela)
- [Comparação com useCommand](#comparação-com-usecommand)
- [Exemplos Práticos](#exemplos-práticos)
- [Casos de Uso Avançados](#casos-de-uso-avançados)
## 🎯 Visão Geral
O `useInvoker` e sua família de hooks especializados fornecem acesso direto e simplificado aos comandos:
```tsx
// useCommand - precisa chamar .invoke()
const command = useCommand("file:save");
await command.invoke(data);
// useInvoker - executa diretamente!
const save = useInvoker("file:save");
await save(data); // 🎯 Direto ao ponto!
// Família de hooks especializados
const logout = useAction("user:logout"); // Sem parâmetros
await logout();
const safeCall = useSafeInvoker("api:risky"); // Nunca lança erro
const result = await safeCall();
const saveUser = useBoundInvoker("user:save", {
// Input pré-configurado
createdBy: currentUser.id,
});
await saveUser({ name: "João" });
const toggle = useToggleInvoker("ui:dark-mode"); // Estados booleanos
await toggle(true);
const batch = useBatchInvoker([
// Execução sequencial
"validate",
"process",
"save",
]);
await batch(inputs);
const parallel = useParallelInvoker([
// Execução paralela
"api:users",
"api:stats",
"api:logs",
]);
const results = await parallel();
```
## 🔧 Hook Principal - useInvoker
### Assinatura
```tsx
useInvoker<TInput, TOutput>(
key: CommandKey,
options?: UseInvokerOptions<TInput, TOutput>
): (input?: TInput) => Promise<TOutput>
// SEMPRE retorna uma função de execução direta
```
### Configurações Disponíveis
```tsx
interface UseInvokerOptions<TInput, TOutput> {
// Execução
source?: CommandSource; // 'api' | 'palette' | 'shortcut'
throwOnError?: boolean; // Default: true
// Input
defaultInput?: Partial<TInput>; // Input padrão mesclado
// Callbacks
onSuccess?: (result: TOutput) => void;
onError?: (error: Error) => void;
}
```
> **Nota**: Para funcionalidades avançadas como retry, debounce, throttle, validação ou transformação, use `useCommand` diretamente.
### Exemplos Básicos
```tsx
// 1. Execução simples
function LogoutButton() {
const logout = useInvoker("user:logout");
return <button onClick={() => logout()}>Sair</button>;
}
// 2. Com input
function SaveButton({ document }) {
const saveDoc = useInvoker<SaveInput, SaveResult>("document:save");
const handleSave = async () => {
const result = await saveDoc({
id: document.id,
content: document.content,
});
toast.success(`Salvo: ${result.filename}`);
};
return <button onClick={handleSave}>Salvar</button>;
}
// 3. Com configurações
function SmartUploader() {
const upload = useInvoker("file:upload", {
throwOnError: false,
defaultInput: { quality: "high" },
onSuccess: (result) => {
analytics.track("file_uploaded", { size: result.size });
},
onError: (error) => {
notification.error("Upload falhou: " + error.message);
},
});
const handleUpload = async (file: File) => {
const result = await upload({ file });
if (result) {
setUploadedFiles((prev) => [...prev, result]);
}
};
}
// 4. Quando precisar de estado, use useCommand
function FileUploader() {
const uploader = useCommand("file:upload", {
onSuccess: (result) => toast.success("Upload concluído!"),
});
return (
<div>
<input
type="file"
onChange={(e) => uploader.invoke({ file: e.target.files[0] })}
disabled={uploader.isLoading}
/>
{uploader.isLoading && <Spinner />}
{uploader.lastError && (
<div className="error">{uploader.lastError.message}</div>
)}
</div>
);
}
```
## 🎪 Hooks Especializados
### 1. useAction - Para Comandos Sem Parâmetros
Otimizado para comandos que não recebem input (ou sempre usam o mesmo input).
```tsx
useAction(key: CommandKey): () => Promise<TOutput>
```
#### Exemplos
```tsx
// Ações simples
function NavigationButtons() {
const goHome = useAction("navigation:home");
const openSettings = useAction("modal:settings");
const refreshData = useAction("data:refresh");
return (
<div className="flex gap-2">
<button onClick={goHome}>🏠 Home</button>
<button onClick={openSettings}>⚙️ Configurações</button>
<button onClick={refreshData}>🔄 Atualizar</button>
</div>
);
}
// Comandos do sistema
function SystemActions() {
const clearCache = useAction("system:clear-cache");
const exportLogs = useAction("system:export-logs");
const runHealthCheck = useAction("system:health-check");
const handleClearCache = async () => {
if (confirm("Limpar cache?")) {
await clearCache();
toast.success("Cache limpo!");
}
};
return (
<div className="admin-panel">
<button onClick={handleClearCache}>Limpar Cache</button>
<button onClick={exportLogs}>Exportar Logs</button>
<button onClick={runHealthCheck}>Health Check</button>
</div>
);
}
// Ações de usuário
function UserActions() {
const logout = useAction("user:logout");
const deleteAccount = useAction("user:delete-account");
const handleDeleteAccount = async () => {
const confirmed = await showConfirmDialog({
title: "Deletar Conta",
message: "Esta ação é irreversível!",
confirmText: "Deletar",
cancelText: "Cancelar",
});
if (confirmed) {
await deleteAccount();
// Usuário será redirecionado automaticamente
}
};
return (
<div className="user-menu">
<button onClick={logout}>Sair</button>
<button onClick={handleDeleteAccount} className="danger">
Deletar Conta
</button>
</div>
);
}
```
### 2. useCommandState - Com Estado Completo
Simplesmente um alias para `useCommand`, mantido para compatibilidade.
```tsx
useCommandState<TInput, TOutput>(
key: CommandKey,
options?: UseInvokerOptions
): CommandInvoker<TInput, TOutput>
```
> **Nota**: Com a refatoração, `useCommandState` é funcionalmente idêntico a `useCommand`. Considere usar `useCommand` diretamente.
#### Exemplos
```tsx
// Monitor de operações
function OperationMonitor() {
const backup = useCommandState("system:backup");
const sync = useCommandState("data:sync");
const cleanup = useCommandState("system:cleanup");
const operations = [
{ name: "Backup", command: backup },
{ name: "Sincronização", command: sync },
{ name: "Limpeza", command: cleanup },
];
return (
<div className="operations-panel">
<h3>Status das Operações</h3>
{operations.map(({ name, command }) => (
<div key={name} className="operation-row">
<span>{name}</span>
{command.isLoading && <Spinner />}
{command.lastExecution && (
<span className="last-run">
Última execução: {formatDistanceToNow(command.lastExecution)}
</span>
)}
{command.lastError && (
<span className="error">❌ {command.lastError.message}</span>
)}
<button onClick={() => command.invoke()} disabled={command.isLoading}>
Executar
</button>
</div>
))}
</div>
);
}
// Dashboard com métricas
function SystemDashboard() {
const healthCheck = useCommandState("system:health-check");
const getMetrics = useCommandState("system:metrics");
// Auto-refresh a cada 30 segundos
useInterval(() => {
if (!healthCheck.isLoading) {
healthCheck.invoke();
}
if (!getMetrics.isLoading) {
getMetrics.invoke();
}
}, 30000);
return (
<div className="dashboard">
<div className="health-card">
<h4>System Health</h4>
{healthCheck.isLoading && <Spinner />}
{healthCheck.lastResult && (
<div className={`status ${healthCheck.lastResult.status}`}>
{healthCheck.lastResult.status.toUpperCase()}
</div>
)}
<div className="stats">
<span>Checks realizados: {healthCheck.executionCount}</span>
</div>
</div>
<div className="metrics-card">
<h4>Métricas</h4>
{getMetrics.lastResult && (
<div className="metrics">
<div>CPU: {getMetrics.lastResult.cpu}%</div>
<div>Memória: {getMetrics.lastResult.memory}%</div>
<div>Disco: {getMetrics.lastResult.disk}%</div>
</div>
)}
</div>
</div>
);
}
```
### 3. useSafeInvoker - Execução Segura
Nunca lança erros, sempre retorna `ExecutionResult`.
```tsx
useSafeInvoker<TInput, TOutput>(
key: CommandKey,
options?: Omit<UseInvokerOptions, 'throwOnError'>
): (input?: TInput) => Promise<ExecutionResult<TOutput>>
interface ExecutionResult<T> {
success: boolean
result?: T
error?: Error
command: CommandKey
}
```
#### Exemplos
```tsx
// API calls não-críticas
function OptionalFeatures() {
const loadRecommendations = useSafeInvoker("ai:recommendations");
const loadAnalytics = useSafeInvoker("analytics:load");
const [recommendations, setRecommendations] = useState(null);
const [analytics, setAnalytics] = useState(null);
useEffect(() => {
// Carrega recursos opcionais sem quebrar a UI
Promise.all([
loadRecommendations({ userId: currentUser.id }),
loadAnalytics({ period: "7d" }),
]).then(([recResult, analyticsResult]) => {
if (recResult.success) {
setRecommendations(recResult.result);
}
if (analyticsResult.success) {
setAnalytics(analyticsResult.result);
}
});
}, []);
return (
<div className="optional-features">
{recommendations && <RecommendationsWidget data={recommendations} />}
{analytics && <AnalyticsWidget data={analytics} />}
</div>
);
}
// Operações em batch com falhas toleradas
function BatchProcessor() {
const processItem = useSafeInvoker("item:process");
const [results, setResults] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
const processBatch = async (items) => {
setIsProcessing(true);
const results = [];
for (const item of items) {
const result = await processItem({ item });
results.push({
item,
success: result.success,
data: result.result,
error: result.error?.message,
});
}
setResults(results);
setIsProcessing(false);
const successCount = results.filter((r) => r.success).length;
toast.success(`${successCount}/${items.length} itens processados`);
};
return (
<div className="batch-processor">
<button
onClick={() => processBatch(selectedItems)}
disabled={isProcessing}
>
{isProcessing ? "Processando..." : "Processar Lote"}
</button>
{results.length > 0 && (
<div className="results">
{results.map((result, index) => (
<div
key={index}
className={`result ${result.success ? "success" : "error"}`}
>
<span>{result.item.name}</span>
{result.success ? (
<span className="success">✅ Sucesso</span>
) : (
<span className="error">❌ {result.error}</span>
)}
</div>
))}
</div>
)}
</div>
);
}
// Fallbacks e degradação graciosa
function DataVisualization() {
const loadLiveData = useSafeInvoker("data:live");
const loadCachedData = useSafeInvoker("data:cached");
const [data, setData] = useState(null);
const [dataSource, setDataSource] = useState("loading");
useEffect(() => {
const loadData = async () => {
// Tenta dados ao vivo primeiro
const liveResult = await loadLiveData();
if (liveResult.success) {
setData(liveResult.result);
setDataSource("live");
return;
}
// Fallback para dados em cache
const cachedResult = await loadCachedData();
if (cachedResult.success) {
setData(cachedResult.result);
setDataSource("cached");
} else {
setDataSource("unavailable");
}
};
loadData();
}, []);
if (dataSource === "loading") {
return <div>Carregando dados...</div>;
}
if (dataSource === "unavailable") {
return <div>Dados indisponíveis no momento</div>;
}
return (
<div className="data-viz">
{dataSource === "cached" && (
<div className="warning">
⚠️ Exibindo dados em cache (dados ao vivo indisponíveis)
</div>
)}
<DataChart data={data} />
</div>
);
}
```
### 4. useBoundInvoker - Input Pré-configurado
Combina input padrão com input fornecido na execução.
```tsx
useBoundInvoker<TInput, TOutput>(
key: CommandKey,
boundInput: Partial<TInput>,
options?: Omit<UseInvokerOptions, 'defaultInput'>
): (input?: Partial<TInput>) => Promise<TOutput>
```
#### Exemplos
```tsx
// Usuário atual em contexto
function CurrentUserActions() {
const currentUser = useCurrentUser();
// Input sempre inclui userId atual
const updateProfile = useBoundInvoker("user:update", {
userId: currentUser.id,
});
const changePassword = useBoundInvoker("user:change-password", {
userId: currentUser.id,
});
const deleteAccount = useBoundInvoker("user:delete", {
userId: currentUser.id,
});
const handleUpdateProfile = async (profileData) => {
// userId é adicionado automaticamente
await updateProfile({
name: profileData.name,
email: profileData.email,
// userId já está incluído
});
};
const handleChangePassword = async (passwords) => {
await changePassword({
currentPassword: passwords.current,
newPassword: passwords.new,
// userId já está incluído
});
};
}
// Contexto de workspace
function WorkspaceCommands() {
const { currentWorkspace } = useWorkspace();
// Comandos sempre executam no workspace atual
const createProject = useBoundInvoker("project:create", {
workspaceId: currentWorkspace.id,
createdBy: getCurrentUser().id,
});
const inviteUser = useBoundInvoker("workspace:invite", {
workspaceId: currentWorkspace.id,
invitedBy: getCurrentUser().id,
});
const updateSettings = useBoundInvoker("workspace:update-settings", {
workspaceId: currentWorkspace.id,
});
return (
<div className="workspace-actions">
<button
onClick={() =>
createProject({
name: "Novo Projeto",
template: "web-app",
})
}
>
Criar Projeto
</button>
<button
onClick={() =>
inviteUser({
email: "user@example.com",
role: "member",
})
}
>
Convidar Usuário
</button>
</div>
);
}
// Configurações de API
function ApiCommands() {
const apiConfig = useApiConfig();
// Todas as chamadas incluem configuração padrão
const apiCall = useBoundInvoker("api:call", {
baseUrl: apiConfig.baseUrl,
timeout: apiConfig.timeout,
retries: 3,
headers: {
Authorization: `Bearer ${apiConfig.token}`,
"User-Agent": "MyApp/1.0",
},
});
const uploadFile = useBoundInvoker("api:upload", {
baseUrl: apiConfig.baseUrl,
headers: {
Authorization: `Bearer ${apiConfig.token}`,
},
timeout: 60000, // Upload tem timeout maior
});
// Uso simplificado
const fetchUsers = () =>
apiCall({
endpoint: "/users",
method: "GET",
});
const createUser = (userData) =>
apiCall({
endpoint: "/users",
method: "POST",
body: userData,
});
const uploadAvatar = (file) =>
uploadFile({
endpoint: "/avatars",
file: file,
});
}
// Formulários com dados parciais
function FormCommands() {
const { formData } = useFormContext();
// Salvamento sempre inclui dados base do formulário
const saveStep = useBoundInvoker("form:save-step", {
formId: formData.id,
userId: formData.userId,
sessionId: formData.sessionId,
});
const validateStep = useBoundInvoker("form:validate-step", {
formId: formData.id,
formType: formData.type,
});
// Uso nos componentes de step
const saveStep1 = (stepData) =>
saveStep({
step: 1,
data: stepData,
});
const saveStep2 = (stepData) =>
saveStep({
step: 2,
data: stepData,
});
const validateCurrentStep = (stepNumber, data) =>
validateStep({
step: stepNumber,
data: data,
});
}
```
### 5. useToggleInvoker - Comandos Toggle
Especializado para comandos que trabalham com estados booleanos.
```tsx
useToggleInvoker(
key: CommandKey,
options?: Omit<UseInvokerOptions<boolean, boolean>, 'defaultInput'>
): (state?: boolean) => Promise<boolean>
```
#### Exemplos
```tsx
// Sistema de preferências
function UserPreferences() {
const toggleDarkMode = useToggleInvoker("preferences:dark-mode");
const toggleNotifications = useToggleInvoker("preferences:notifications");
const toggleAutoSave = useToggleInvoker("preferences:auto-save");
const [isDarkMode, setIsDarkMode] = useState(false);
const [hasNotifications, setHasNotifications] = useState(true);
const [hasAutoSave, setHasAutoSave] = useState(true);
const handleDarkModeToggle = async () => {
const newState = await toggleDarkMode(!isDarkMode);
setIsDarkMode(newState);
};
const handleNotificationsToggle = async () => {
const newState = await toggleNotifications(!hasNotifications);
setHasNotifications(newState);
};
return (
<div className="preferences">
<label>
<input
type="checkbox"
checked={isDarkMode}
onChange={handleDarkModeToggle}
/>
Modo Escuro
</label>
<label>
<input
type="checkbox"
checked={hasNotifications}
onChange={handleNotificationsToggle}
/>
Notificações
</label>
<label>
<input
type="checkbox"
checked={hasAutoSave}
onChange={() => toggleAutoSave(!hasAutoSave).then(setHasAutoSave)}
/>
Salvamento Automático
</label>
</div>
);
}
// Controles de visibilidade
function VisibilityControls() {
const toggleSidebar = useToggleInvoker("ui:toggle-sidebar");
const toggleDevTools = useToggleInvoker("ui:toggle-devtools");
const toggleFullscreen = useToggleInvoker("ui:toggle-fullscreen");
return (
<div className="ui-controls">
<button onClick={() => toggleSidebar()}>Alternar Barra Lateral</button>
<button onClick={() => toggleDevTools()}>DevTools</button>
<button onClick={() => toggleFullscreen()}>Tela Cheia</button>
</div>
);
}
// Feature flags
function FeatureFlags() {
const toggleFeature = useToggleInvoker("feature:toggle");
const [features, setFeatures] = useState({
newDashboard: false,
betaEditor: false,
experimentalApi: false,
});
const handleToggleFeature = async (featureName) => {
const newState = await toggleFeature({
feature: featureName,
enabled: !features[featureName],
});
setFeatures((prev) => ({
...prev,
[featureName]: newState,
}));
};
return (
<div className="feature-flags">
<h3>Features Experimentais</h3>
{Object.entries(features).map(([feature, enabled]) => (
<div key={feature}>
<label>
<input
type="checkbox"
checked={enabled}
onChange={() => handleToggleFeature(feature)}
/>
{feature}
</label>
</div>
))}
</div>
);
}
```
### 6. useBatchInvoker - Execução Sequencial
Executa múltiplos comandos em sequência, com controle de erro e progresso.
```tsx
useBatchInvoker(
keys: CommandKey[],
options?: {
source?: CommandSource
stopOnError?: boolean
onProgress?: (completed: number, total: number) => void
}
): (inputs?: any[], batchOptions?: { stopOnError?: boolean }) => Promise<ExecutionResult[]>
```
#### Exemplos
```tsx
// Processamento de múltiplos arquivos
function BatchFileProcessor() {
const processFiles = useBatchInvoker(
["file:validate", "file:optimize", "file:upload", "file:index"],
{
stopOnError: true,
onProgress: (completed, total) => {
console.log(`Processado ${completed}/${total}`);
},
},
);
const [progress, setProgress] = useState({ completed: 0, total: 0 });
const [results, setResults] = useState([]);
const handleProcessFiles = async (files) => {
const inputs = [
{ files }, // para validate
{ files }, // para optimize
{ files }, // para upload
{ uploadedFiles: files }, // para index
];
const batchResults = await processFiles(inputs, {
stopOnError: false, // continua mesmo com erros
});
setResults(batchResults);
const successful = batchResults.filter((r) => r.success).length;
toast.info(`${successful}/${batchResults.length} operações concluídas`);
};
return (
<div className="batch-processor">
<FileDropzone onDrop={handleProcessFiles} />
{results.length > 0 && (
<div className="results">
<h4>Resultados do Processamento:</h4>
{results.map((result, idx) => (
<div key={idx} className={result.success ? "success" : "error"}>
Etapa {idx + 1}: {result.success ? "✅" : "❌"}
{result.error && <span>{result.error.message}</span>}
</div>
))}
</div>
)}
</div>
);
}
// Pipeline de implantação
function DeploymentPipeline() {
const deploySteps = useBatchInvoker(
[
"build:compile",
"test:unit",
"test:integration",
"deploy:staging",
"test:e2e",
"deploy:production",
],
{
stopOnError: true, // para na primeira falha
onProgress: (step, total) => {
updateDeploymentStatus({
currentStep: step,
totalSteps: total,
percentage: (step / total) * 100,
});
},
},
);
const [isDeploying, setIsDeploying] = useState(false);
const [deploymentLog, setDeploymentLog] = useState([]);
const startDeployment = async () => {
setIsDeploying(true);
setDeploymentLog([]);
const config = {
branch: "main",
environment: "production",
version: "2.0.0",
};
// Cada comando recebe a mesma config
const inputs = Array(6).fill(config);
const results = await deploySteps(inputs);
// Processa resultados
results.forEach((result, idx) => {
const stepName = [
"Compilação",
"Testes Unitários",
"Testes de Integração",
"Deploy Staging",
"Testes E2E",
"Deploy Produção",
][idx];
setDeploymentLog((prev) => [
...prev,
{
step: stepName,
success: result.success,
error: result.error?.message,
timestamp: new Date(),
},
]);
});
setIsDeploying(false);
const allSuccessful = results.every((r) => r.success);
if (allSuccessful) {
toast.success("Deploy concluído com sucesso!");
} else {
toast.error("Deploy falhou!");
}
};
return (
<div className="deployment-pipeline">
<button onClick={startDeployment} disabled={isDeploying}>
{isDeploying ? "Deployment em progresso..." : "Iniciar Deploy"}
</button>
<div className="deployment-log">
{deploymentLog.map((entry, idx) => (
<div
key={idx}
className={`log-entry ${entry.success ? "success" : "error"}`}
>
<span>{entry.step}</span>
<span>{entry.success ? "✅" : "❌"}</span>
{entry.error && <span className="error-msg">{entry.error}</span>}
</div>
))}
</div>
</div>
);
}
// Wizard multi-etapas
function SetupWizard() {
const setupSteps = useBatchInvoker([
"setup:check-requirements",
"setup:install-dependencies",
"setup:configure-database",
"setup:create-admin",
"setup:initial-data",
]);
const [currentStep, setCurrentStep] = useState(0);
const [setupData, setSetupData] = useState({});
const runSetup = async () => {
const inputs = [
{ os: navigator.platform },
{ packageManager: "npm" },
{ database: setupData.database },
{ admin: setupData.admin },
{ sampleData: setupData.includeSampleData },
];
const results = await setupSteps(inputs, {
stopOnError: true,
});
if (results.every((r) => r.success)) {
router.push("/dashboard");
}
};
}
```
### 7. useParallelInvoker - Execução Paralela
Executa múltiplos comandos em paralelo, retornando Promise.allSettled results.
```tsx
useParallelInvoker(
keys: CommandKey[],
options?: { source?: CommandSource }
): (inputs?: any[]) => Promise<PromiseSettledResult<any>[]>
```
#### Exemplos
```tsx
// Carregamento paralelo de dados do dashboard
function DashboardData() {
const loadDashboardData = useParallelInvoker([
"stats:revenue",
"stats:users",
"stats:orders",
"chart:sales",
"notifications:unread",
]);
const [dashboardData, setDashboardData] = useState({
revenue: null,
users: null,
orders: null,
salesChart: null,
notifications: null,
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
setIsLoading(true);
// Inputs para cada comando
const inputs = [
{ period: "30d" }, // revenue
{ status: "active" }, // users
{ status: "pending" }, // orders
{ range: "last-month" }, // sales chart
{ limit: 10 }, // notifications
];
const results = await loadDashboardData(inputs);
// Processa resultados
setDashboardData({
revenue: results[0].status === "fulfilled" ? results[0].value : null,
users: results[1].status === "fulfilled" ? results[1].value : null,
orders: results[2].status === "fulfilled" ? results[2].value : null,
salesChart: results[3].status === "fulfilled" ? results[3].value : null,
notifications:
results[4].status === "fulfilled" ? results[4].value : null,
});
setIsLoading(false);
// Log de erros
results.forEach((result, idx) => {
if (result.status === "rejected") {
console.error(
`Falha ao carregar ${["revenue", "users", "orders", "chart", "notifications"][idx]}:`,
result.reason,
);
}
});
};
loadData();
}, []);
if (isLoading) return <DashboardSkeleton />;
return (
<div className="dashboard">
{dashboardData.revenue && <RevenueCard data={dashboardData.revenue} />}
{dashboardData.users && <UsersCard data={dashboardData.users} />}
{dashboardData.orders && <OrdersCard data={dashboardData.orders} />}
{dashboardData.salesChart && (
<SalesChart data={dashboardData.salesChart} />
)}
{dashboardData.notifications && (
<NotificationsList data={dashboardData.notifications} />
)}
</div>
);
}
// Busca em múltiplas fontes
function UniversalSearch() {
const searchAllSources = useParallelInvoker([
"search:users",
"search:products",
"search:orders",
"search:documents",
"search:help",
]);
const [searchResults, setSearchResults] = useState({});
const [isSearching, setIsSearching] = useState(false);
const handleSearch = async (query) => {
if (!query.trim()) return;
setIsSearching(true);
// Mesmo query para todas as fontes
const inputs = Array(5).fill({ query, limit: 5 });
const results = await searchAllSources(inputs);
const processedResults = {
users: results[0].status === "fulfilled" ? results[0].value : [],
products: results[1].status === "fulfilled" ? results[1].value : [],
orders: results[2].status === "fulfilled" ? results[2].value : [],
documents: results[3].status === "fulfilled" ? results[3].value : [],
help: results[4].status === "fulfilled" ? results[4].value : [],
};
setSearchResults(processedResults);
setIsSearching(false);
};
return (
<div className="universal-search">
<SearchInput
onSearch={handleSearch}
placeholder="Buscar em todo o sistema..."
/>
{isSearching && <SearchSpinner />}
<div className="search-results">
{Object.entries(searchResults).map(
([category, items]) =>
items.length > 0 && (
<div key={category} className="result-category">
<h4>{category}</h4>
{items.map((item) => (
<SearchResultItem key={item.id} item={item} />
))}
</div>
),
)}
</div>
</div>
);
}
// Validação paralela de formulário
function ComplexFormValidation() {
const validateFields = useParallelInvoker([
"validate:email",
"validate:cpf",
"validate:phone",
"validate:address",
"validate:credit-card",
]);
const [errors, setErrors] = useState({});
const [isValidating, setIsValidating] = useState(false);
const validateForm = async (formData) => {
setIsValidating(true);
const inputs = [
{ email: formData.email },
{ cpf: formData.cpf },
{ phone: formData.phone },
{ address: formData.address },
{ creditCard: formData.creditCard },
];
const results = await validateFields(inputs);
const validationErrors = {};
const fieldNames = ["email", "cpf", "phone", "address", "creditCard"];
results.forEach((result, idx) => {
if (
result.status === "rejected" ||
(result.status === "fulfilled" && !result.value.isValid)
) {
validationErrors[fieldNames[idx]] =
result.status === "rejected"
? "Erro ao validar campo"
: result.value.error;
}
});
setErrors(validationErrors);
setIsValidating(false);
return Object.keys(validationErrors).length === 0;
};
return { validateForm, errors, isValidating };
}
```
## 🆚 Comparação com useCommand
| Aspecto | useCommand | useInvoker |
| ------------------ | ----------------------- | ----------------- |
| **Retorno** | CommandInvoker (objeto) | Função direta |
| **Uso** | `cmd.invoke(data)` | `cmd(data)` |
| **Estado** | Sempre disponível | Use `useCommand` |
| **Configurações** | Todas disponíveis | Essenciais apenas |
| **Retry/Debounce** | ✅ Sim | ❌ Não |
| **Transformações** | ✅ Sim | ❌ Não |
| **Casos de Uso** | Complexos | Simples e diretos |
### Quando Usar Cada Um
```tsx
// Use useCommand quando precisar de:
// ✅ Estado (isLoading, lastResult, etc)
// ✅ Múltiplos métodos (invoke, attempt, execute)
// ✅ Configurações avançadas (retry, debounce, throttle)
// ✅ Transformações de dados
// ✅ Validação de input
const command = useCommand("complex:operation", {
retry: 3,
debounce: 500,
transformInput: (input) => ({ ...input, timestamp: Date.now() }),
validateInput: (input) => (input.name ? true : "Nome obrigatório"),
});
// Acesso ao estado
console.log(command.isLoading);
console.log(command.lastResult);
// Múltiplos métodos
await command.invoke(data); // Lança erro se falhar
await command.attempt(data); // Retorna ExecutionResult
await command.execute(data); // Com callbacks inline
// Use useInvoker quando quiser:
// ✅ Execução direta e simples
// ✅ Sintaxe mínima
// ✅ Sem necessidade de estado
// ✅ Performance otimizada
const save = useInvoker("file:save");
await save(data); // Simples e direto!
// Para ações específicas
const logout = useAction("user:logout");
await logout();
const safeCall = useSafeInvoker("risky:operation");
const result = await safeCall(); // Nunca lança erro
```
## 🎯 Casos de Uso Avançados
### 1. Sistema de Notificações
```tsx
function NotificationSystem() {
// Ações diretas para tipos de notificação
const showSuccess = useAction("notification:success");
const showError = useAction("notification:error");
const showWarning = useAction("notification:warning");
// Notificação customizada com input bound
const showCustom = useBoundInvoker("notification:custom", {
duration: 5000,
position: "top-right",
dismissible: true,
});
// Toast com estado para controle
const toast = useCommandState("notification:toast");
const notify = {
success: (message) => showSuccess({ message }),
error: (message) => showError({ message }),
warning: (message) => showWarning({ message }),
custom: (message, options = {}) =>
showCustom({
message,
...options,
}),
progress: (message) =>
toast.invoke({
type: "progress",
message,
showProgress: true,
}),
};
return { notify, toast };
}
// Uso no app
function App() {
const { notify } = useNotificationSystem();
const handleSave = async () => {
notify.progress("Salvando documento...");
try {
await saveDocument();
notify.success("Documento salvo!");
} catch (error) {
notify.error("Erro ao salvar: " + error.message);
}
};
}
```
### 2. Sistema de Cache Inteligente
```tsx
function SmartCache() {
// Cache hit/miss com estado
const cacheGet = useCommandState("cache:get");
const cacheSet = useSafeInvoker("cache:set");
const cacheInvalidate = useAction("cache:invalidate");
const useCache = (key, fetcher, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
setLoading(true);
// Tenta cache primeiro
const cached = await cacheGet.invoke({ key });
if (cached && !isExpired(cached, options.ttl)) {
setData(cached.data);
setLoading(false);
return;
}
// Cache miss - busca dados
try {
const freshData = await fetcher();
setData(freshData);
// Salva no cache (safe - não quebra se falhar)
await cacheSet({
key,
data: freshData,
timestamp: Date.now(),
});
} catch (error) {
console.error("Fetch failed:", error);
} finally {
setLoading(false);
}
};
loadData();
}, [key]);
const invalidate = () => {
cacheInvalidate({ key });
setData(null);
};
return { data, loading, invalidate, cacheHit: cacheGet.lastResult };
};
return { useCache };
}
// Uso
function UserProfile({ userId }) {
const { useCache } = useSmartCache();
const {
data: user,
loading,
invalidate,
} = useCache(
`user:${userId}`,
() => fetchUser(userId),
{ ttl: 5 * 60 * 1000 }, // 5 minutos
);
if (loading) return <Spinner />;
return (
<div>
<UserInfo user={user} />
<button onClick={invalidate}>Atualizar</button>
</div>
);
}
```
### 3. Upload Manager
```tsx
function UploadManager() {
// Upload único com estado completo
const singleUpload = useCommandState("file:upload-single");
// Upload em lote (seguro para continuar mesmo com falhas)
const batchUpload = useSafeInvoker("file:upload-batch");
// Operações bound ao usuário atual
const currentUser = useCurrentUser();
const uploadToGallery = useBoundInvoker("file:upload-gallery", {
userId: currentUser.id,
gallery: "user-uploads",
});
const uploadToProject = useBoundInvoker("file:upload-project", {
userId: currentUser.id,
});
const uploadStates = new Map();
const uploadFile = async (file, destination = "general") => {
const uploadId = generateId();
switch (destination) {
case "gallery":
return await uploadToGallery({ file, uploadId });
case "project":
const projectId = getCurrentProject().id;
return await uploadToProject({ file, uploadId, projectId });
default:
return await singleUpload.invoke({ file, uploadId });
}
};
const uploadMultiple = async (files) => {
const result = await batchUpload(
files.map((file) => ({
file,
uploadId: generateId(),
})),
);
if (result.success) {
return result.result;
} else {
// Processa resultados parciais
const { successful, failed } = result.error.partialResults || {};
toast.info(
`${successful?.length || 0} arquivos enviados, ${failed?.length || 0} falharam`,
);
return { successful, failed };
}
};
return {
uploadFile,
uploadMultiple,
singleUploadState: singleUpload,
clearUploadHistory: () => singleUpload.reset(),
};
}
// Componente de upload
function FileUploadZone() {
const { uploadFile, uploadMultiple, singleUploadState } = useUploadManager();
const handleDrop = async (files) => {
if (files.length === 1) {
await uploadFile(files[0], "gallery");
} else {
await uploadMultiple(files);
}
};
return (
<div
className="upload-zone"
onDrop={handleDrop}
data-uploading={singleUploadState.isLoading}
>
{singleUploadState.isLoading ? (
<div className="upload-progress">
<Spinner />
<span>Enviando arquivo...</span>
</div>
) : (
<div className="upload-hint">
Arraste arquivos aqui ou clique para selecionar
</div>
)}
{singleUploadState.lastError && (
<div className="upload-error">
❌ {singleUploadState.lastError.message}
<button onClick={singleUploadState.clearError}>×</button>
</div>
)}
</div>
);
}
```
## 🎭 Patterns Avançados
### 1. Command Factory Pattern
```tsx
function useCommandFactory() {
const createCRUDCommands = (entity) => ({
create: useBoundInvoker(`${entity}:create`, {
createdBy: getCurrentUser().id,
}),
update: useBoundInvoker(`${entity}:update`, {
updatedBy: getCurrentUser().id,
}),
delete: useSafeInvoker(`${entity}:delete`),
list: useCommandState(`${entity}:list`),
get: useInvoker(`${entity}:get`),
});
return { createCRUDCommands };
}
// Uso
function UserManagement() {
const { createCRUDCommands } = useCommandFactory();
const userCommands = createCRUDCommands("user");
return (
<div>
<button onClick={() => userCommands.create({ name: "João" })}>
Criar Usuário
</button>
<button onClick={() => userCommands.list.invoke()}>
Listar Usuários
</button>
{userCommands.list.isLoading && <Spinner />}
</div>
);
}
```
### 2. State Machine Integration
```tsx
function useWorkflowState() {
const transitions = {
start: useAction("workflow:start"),
pause: useAction("workflow:pause"),
resume: useAction("workflow:resume"),
complete: useAction("workflow:complete"),
cancel: useAction("workflow:cancel"),
};
const status = useCommandState("workflow:status");
const canTransition = (from, to) => {
const validTransitions = {
idle: ["start"],
running: ["pause", "complete", "cancel"],
paused: ["resume", "cancel"],
completed: ["start"],
cancelled: ["start"],
};
return validTransitions[from]?.includes(to) || false;
};
return { transitions, status, canTransition };
}
```
## 🎯 Resumo
A família `useInvoker` fornece acesso direto e simplificado aos comandos:
### Hooks de Execução Direta
- **`useInvoker`** - Retorna diretamente a função de execução
- **`useAction`** - Para comandos sem parâmetros
- **`useSafeInvoker`** - Execução que nunca lança erro
- **`useBoundInvoker`** - Com input pré-configurado
- **`useToggleInvoker`** - Para comandos toggle (boolean)
### Hooks de Execução em Lote
- **`useBatchInvoker`** - Execução sequencial com controle de progresso
- **`useParallelInvoker`** - Execução paralela com Promise.allSettled
### Hook de Estado
- **`useCommandState`** - Alias para useCommand (considere usar useCommand diretamente)
Para casos que necessitam de estado, múltiplos métodos de execução ou configurações avançadas, use `useCommand` diretamente.