UNPKG

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