UNPKG

@darksnow-ui/commander

Version:

Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs

1,590 lines (1,295 loc) 41.4 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.