# Gestión de Estado ## React Query para Server State ```typescript // hooks/useTasks.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '@/api/client' import { Task, CreateTaskInput, UpdateTaskInput } from '@/types/task' import toast from 'react-hot-toast' export function useTasks(filters?: { projectId?: string; state?: string }) { return useQuery({ queryKey: ['tasks', filters], queryFn: async () => { const { data } = await api.get<{ tasks: Task[] }>('/tasks', { params: filters }) return data.tasks }, }) } export function useTask(taskId: string) { return useQuery({ queryKey: ['tasks', taskId], queryFn: async () => { const { data } = await api.get<{ task: Task }>(`/tasks/${taskId}`) return data.task }, enabled: !!taskId, }) } export function useCreateTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (input: CreateTaskInput) => { const { data } = await api.post('/tasks', input) return data }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tasks'] }) }, }) } export function useUpdateTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ taskId, updates }: { taskId: string; updates: UpdateTaskInput }) => { const { data } = await api.patch(`/tasks/${taskId}`, updates) return data }, onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: ['tasks'] }) queryClient.invalidateQueries({ queryKey: ['tasks', variables.taskId] }) }, }) } export function useRespondToQuestion() { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ taskId, questionId, response, }: { taskId: string questionId: string response: string }) => { const { data } = await api.post(`/tasks/${taskId}/respond`, { questionId, response, }) return data }, onSuccess: (_, variables) => { toast.success('Respuesta enviada') queryClient.invalidateQueries({ queryKey: ['tasks', variables.taskId] }) }, }) } export function useApproveTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (taskId: string) => { const { data } = await api.post(`/tasks/${taskId}/approve`) return data }, onSuccess: () => { toast.success('Tarea aprobada') queryClient.invalidateQueries({ queryKey: ['tasks'] }) }, }) } ``` ```typescript // hooks/useProjects.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '@/api/client' import { Project, CreateProjectInput } from '@/types/project' export function useProjects() { return useQuery({ queryKey: ['projects'], queryFn: async () => { const { data } = await api.get<{ projects: Project[] }>('/projects') return data.projects }, }) } export function useProject(projectId: string) { return useQuery({ queryKey: ['projects', projectId], queryFn: async () => { const { data } = await api.get<{ project: Project }>(`/projects/${projectId}`) return data.project }, enabled: !!projectId, }) } export function useCreateProject() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (input: CreateProjectInput) => { const { data } = await api.post('/projects', input) return data }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects'] }) }, }) } ``` ```typescript // hooks/useAgents.ts import { useQuery } from '@tanstack/react-query' import { api } from '@/api/client' import { Agent } from '@/types/agent' export function useAgents() { return useQuery({ queryKey: ['agents'], queryFn: async () => { const { data } = await api.get<{ agents: Agent[] }>('/agents') return data.agents }, refetchInterval: 5000, // Refetch every 5s }) } export function useAgent(agentId: string) { return useQuery({ queryKey: ['agents', agentId], queryFn: async () => { const { data } = await api.get<{ agent: Agent }>(`/agents/${agentId}`) return data.agent }, enabled: !!agentId, refetchInterval: 3000, }) } export function useAgentLogs(agentId: string, limit = 100) { return useQuery({ queryKey: ['agents', agentId, 'logs', limit], queryFn: async () => { const { data } = await api.get(`/agents/${agentId}/logs`, { params: { limit }, }) return data.logs }, enabled: !!agentId, }) } ``` ## Zustand para Client State ```typescript // store/authStore.ts import { create } from 'zustand' import { persist } from 'zustand/middleware' interface User { id: string email: string name: string } interface AuthState { user: User | null token: string | null isAuthenticated: boolean login: (token: string, user: User) => void logout: () => void } export const useAuthStore = create()( persist( (set) => ({ user: null, token: null, isAuthenticated: false, login: (token, user) => { set({ token, user, isAuthenticated: true }) }, logout: () => { set({ user: null, token: null, isAuthenticated: false }) }, }), { name: 'auth-storage', } ) ) ``` ```typescript // store/uiStore.ts import { create } from 'zustand' interface UIState { sidebarOpen: boolean activeModal: string | null toggleSidebar: () => void openModal: (modalId: string) => void closeModal: () => void } export const useUIStore = create((set) => ({ sidebarOpen: true, activeModal: null, toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), openModal: (modalId) => set({ activeModal: modalId }), closeModal: () => set({ activeModal: null }), })) ``` ```typescript // store/terminalStore.ts import { create } from 'zustand' interface TerminalTab { id: string agentId: string podName: string isActive: boolean } interface TerminalState { tabs: TerminalTab[] activeTabId: string | null openTerminal: (agentId: string, podName: string) => void closeTerminal: (tabId: string) => void setActiveTab: (tabId: string) => void } export const useTerminalStore = create((set) => ({ tabs: [], activeTabId: null, openTerminal: (agentId, podName) => set((state) => { const existingTab = state.tabs.find((t) => t.agentId === agentId) if (existingTab) { return { tabs: state.tabs.map((t) => ({ ...t, isActive: t.id === existingTab.id, })), activeTabId: existingTab.id, } } const newTab: TerminalTab = { id: `term-${Date.now()}`, agentId, podName, isActive: true, } return { tabs: [ ...state.tabs.map((t) => ({ ...t, isActive: false })), newTab, ], activeTabId: newTab.id, } }), closeTerminal: (tabId) => set((state) => { const newTabs = state.tabs.filter((t) => t.id !== tabId) const newActiveTab = newTabs.length > 0 ? newTabs[0].id : null return { tabs: newTabs.map((t, i) => ({ ...t, isActive: i === 0, })), activeTabId: newActiveTab, } }), setActiveTab: (tabId) => set((state) => ({ tabs: state.tabs.map((t) => ({ ...t, isActive: t.id === tabId, })), activeTabId: tabId, })), })) ``` ## WebSocket Hook ```typescript // hooks/useWebSocket.ts import { useEffect } from 'use' import { useQueryClient } from '@tanstack/react-query' import { io, Socket } from 'socket.io-client' import { useAuthStore } from '@/store/authStore' import toast from 'react-hot-toast' let socket: Socket | null = null export function useWebSocket() { const queryClient = useQueryClient() const token = useAuthStore((state) => state.token) useEffect(() => { if (!token) return // Initialize socket socket = io(import.meta.env.VITE_WS_URL || 'http://localhost:3000', { auth: { token }, }) socket.on('connect', () => { console.log('WebSocket connected') }) socket.on('disconnect', () => { console.log('WebSocket disconnected') }) // Task events socket.on('task:created', (data) => { queryClient.invalidateQueries({ queryKey: ['tasks'] }) toast.success(`Nueva tarea: ${data.title}`) }) socket.on('task:status_changed', (data) => { queryClient.invalidateQueries({ queryKey: ['tasks'] }) queryClient.invalidateQueries({ queryKey: ['tasks', data.taskId] }) if (data.newState === 'ready_to_test') { toast.success('Tarea lista para probar!', { duration: 5000, }) } }) socket.on('task:needs_input', (data) => { queryClient.invalidateQueries({ queryKey: ['tasks', data.taskId] }) toast((t) => (

El agente necesita información

{data.question}

), { duration: 10000, icon: '❓', }) }) socket.on('task:pr_created', (data) => { toast.success('Pull Request creado!', { action: { label: 'Ver PR', onClick: () => window.open(data.prUrl, '_blank'), }, }) }) socket.on('task:ready_to_test', (data) => { toast.success('Preview deploy completado!', { action: { label: 'Ver Preview', onClick: () => window.open(data.previewUrl, '_blank'), }, }) }) // Agent events socket.on('agent:status', (data) => { queryClient.invalidateQueries({ queryKey: ['agents'] }) }) // Deploy events socket.on('deploy:started', (data) => { toast.loading(`Desplegando a ${data.environment}...`, { id: `deploy-${data.deploymentId}`, }) }) socket.on('deploy:completed', (data) => { toast.success(`Deploy completado: ${data.environment}`, { id: `deploy-${data.deploymentId}`, action: { label: 'Abrir', onClick: () => window.open(data.url, '_blank'), }, }) }) socket.on('deploy:failed', (data) => { toast.error(`Deploy falló: ${data.environment}`, { id: `deploy-${data.deploymentId}`, }) }) return () => { if (socket) { socket.disconnect() socket = null } } }, [token, queryClient]) return socket } // Export for manual usage export function getSocket() { return socket } ``` ## API Client ```typescript // api/client.ts import axios from 'axios' import { useAuthStore } from '@/store/authStore' export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api', timeout: 30000, }) // Request interceptor api.interceptors.request.use((config) => { const token = useAuthStore.getState().token if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // Response interceptor api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { useAuthStore.getState().logout() window.location.href = '/login' } return Promise.reject(error) } ) ```