@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
Markdown
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.
- [Visão Geral](
- [Hook Principal](
- [Hooks Especializados](
- [useAction](
- [useCommandState](
- [useSafeInvoker](
- [useBoundInvoker](
- [useToggleInvoker](
- [useBatchInvoker](
- [useParallelInvoker](
- [Comparação com useCommand](
- [Exemplos Práticos](
- [Casos de Uso Avançados](
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()
```
```tsx
useInvoker<TInput, TOutput>(
key: CommandKey,
options?: UseInvokerOptions<TInput, TOutput>
): (input?: TInput) => Promise<TOutput>
// SEMPRE retorna uma função de execução direta
```
```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.
```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>
)
}
```
Otimizado para comandos que não recebem input (ou sempre usam o mesmo input).
```tsx
useAction(key: CommandKey): () => Promise<TOutput>
```
```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>
)
}
```
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
}
```
```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>
)
}
```
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>
```
```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
})
}
```
Especializado para comandos que trabalham com estados booleanos.
```tsx
useToggleInvoker(
key: CommandKey,
options?: Omit<UseInvokerOptions<boolean, boolean>, 'defaultInput'>
): (state?: boolean) => Promise<boolean>
```
```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,
[]: 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>
)
}
```
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[]>
```
```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')
}
}
}
```
Executa múltiplos comandos em paralelo, retornando Promise.allSettled results.
```tsx
useParallelInvoker(
keys: CommandKey[],
options?: { source?: CommandSource }
): (inputs?: any[]) => Promise<PromiseSettledResult<any>[]>
```
```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 }
}
```
| 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 |
```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
```
```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)
}
}
}
```
```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>
)
}
```
```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>
)
}
```
```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>
)
}
```
```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 }
}
```
A família `useInvoker` fornece acesso direto e simplificado aos comandos:
- **`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.