State Management
Nexgent uses a combination of React Query for server state, React Context for client state, and WebSocket for real-time updates.
State Categories
| Category | Technology | Examples |
|---|---|---|
| Server State | React Query | Agents, transactions, signals |
| Client State | React Context | Selected agent, currency preference |
| Real-time State | WebSocket | Positions, prices |
| Form State | React Hook Form | Trading config, login forms |
React Query
React Query manages all server state with automatic caching, refetching, and synchronization.
Provider Setup
// shared/providers/query-provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // Data is fresh for 5 minutes
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
refetchOnWindowFocus: false, // Don't refetch on tab focus
retry: 1, // Retry failed requests once
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}Query Hooks
Feature hooks wrap React Query for specific resources:
// features/agents/hooks/use-agent.ts
export function useAgents(userId?: string) {
return useQuery({
queryKey: ['agents', userId],
queryFn: () => agentsService.getAgents(userId),
enabled: !!userId,
});
}
export function useAgent(agentId?: string) {
return useQuery({
queryKey: ['agents', agentId],
queryFn: () => agentsService.getAgent(agentId!),
enabled: !!agentId,
});
}Mutation Hooks
Mutations handle create/update/delete with cache updates:
export function useCreateAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateAgentRequest) => agentsService.createAgent(data),
onSuccess: (newAgent) => {
// Optimistic cache update
queryClient.setQueryData(['agents', newAgent.userId], (old: Agent[] | undefined) => {
return old ? [...old, newAgent] : [newAgent];
});
// Background invalidation
queryClient.invalidateQueries({ queryKey: ['agents'] });
},
});
}Query Key Patterns
Consistent query keys enable targeted invalidation:
| Pattern | Example |
|---|---|
['agents'] | All agents (any user) |
['agents', userId] | Agents for specific user |
['agents', agentId] | Single agent |
['agent-performance', agentId] | Agent performance metrics |
['agent-historical-swaps', agentId] | Agent trade history |
// Invalidate all agent-related queries
queryClient.invalidateQueries({ queryKey: ['agents'] });
// Invalidate specific agent
queryClient.invalidateQueries({ queryKey: ['agents', agentId] });React Query automatically deduplicates requests. Multiple components using useAgents(userId) will share the same cached data and only trigger one API call.
React Context
Context manages client-side state that doesn't come from the server.
Agent Selection Context
Tracks the currently selected agent across the application:
// shared/contexts/agent-selection.context.tsx
interface AgentSelectionContextType {
selectedAgentId: string | null;
selectedAgent: Agent | null;
agents: Agent[];
isLoading: boolean;
selectAgent: (agentId: string) => void;
}
export function AgentSelectionProvider({ children }: { children: ReactNode }) {
// Initialize from localStorage for persistence across refreshes
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('selectedAgentId');
}
return null;
});
// Fetch agents using React Query
const { data: agents = [], isLoading } = useAgents(user?.id);
// Fetch selected agent details
const { data: selectedAgent } = useAgent(selectedAgentId || undefined);
// Auto-select first agent if none selected
useEffect(() => {
if (agents.length > 0 && !selectedAgentId) {
setSelectedAgentId(agents[0].id);
}
}, [agents, selectedAgentId]);
// Persist to localStorage
useEffect(() => {
if (selectedAgentId) {
localStorage.setItem('selectedAgentId', selectedAgentId);
}
}, [selectedAgentId]);
return (
<AgentSelectionContext.Provider value={{ selectedAgentId, selectedAgent, agents, isLoading, selectAgent }}>
{children}
</AgentSelectionContext.Provider>
);
}
export function useAgentSelection() {
const context = useContext(AgentSelectionContext);
if (!context) {
throw new Error('useAgentSelection must be used within AgentSelectionProvider');
}
return context;
}Usage
function AgentSwitcher() {
const { selectedAgent, agents, selectAgent } = useAgentSelection();
return (
<Select value={selectedAgent?.id} onValueChange={selectAgent}>
{agents.map(agent => (
<SelectItem key={agent.id} value={agent.id}>
{agent.name}
</SelectItem>
))}
</Select>
);
}Other Contexts
| Context | Purpose |
|---|---|
UserContext | Current authenticated user |
CurrencyContext | USD/SOL display preference, SOL price |
TradingModeContext | Simulation/live mode |
WalletContext | Active wallet for selected agent |
WebSocket State
Real-time position and price updates flow through WebSocket.
WebSocket Hook
// infrastructure/websocket/hooks/use-websocket.ts
export function useWebSocket(agentId: string | null): UseWebSocketReturn {
const { data: session } = useSession();
const [positions, setPositions] = useState<LivePosition[]>([]);
const [isConnected, setIsConnected] = useState(false);
// Connect when agentId and session are available
useEffect(() => {
if (!agentId || !session?.accessToken) return;
const ws = new WebSocket(
`${WS_URL}/ws?token=${session.accessToken}&agentId=${agentId}`
);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'initial_data':
setPositions(message.data.positions);
break;
case 'position_update':
handlePositionUpdate(message.data);
break;
case 'price_update_batch':
handlePriceUpdates(message.data.updates);
break;
}
};
return () => ws.close();
}, [agentId, session?.accessToken]);
return { positions, isConnected, connect, disconnect };
}Integration with React Query
WebSocket updates can invalidate React Query caches:
const handlePositionUpdate = (data: PositionUpdateData) => {
if (data.eventType === 'position_closed') {
// Invalidate historical swaps to show new trade
queryClient.invalidateQueries({ queryKey: ['agent-historical-swaps'] });
// Invalidate performance to update metrics
queryClient.invalidateQueries({ queryKey: ['agent-performance'] });
}
// Update local position state
setPositions(prev => updatePositionInArray(prev, data));
};Price Update Batching
Price updates are batched for performance:
const pendingUpdates = useRef<Map<string, PriceUpdate>>(new Map());
const updateTimeout = useRef<NodeJS.Timeout | null>(null);
const queuePriceUpdate = (tokenAddress: string, price: number, priceUsd: number) => {
pendingUpdates.current.set(tokenAddress, { price, priceUsd });
if (updateTimeout.current) return;
// Batch apply every 100ms
updateTimeout.current = setTimeout(() => {
applyPendingUpdates();
updateTimeout.current = null;
}, 100);
};Price updates use requestAnimationFrame for smooth UI updates, preventing jank when many prices change simultaneously.
Form State
React Hook Form manages form state with Zod validation:
// features/agents/components/agent-profile/trading-config-form.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { TradingConfigSchema } from 'nexgent-open-source-trading-engine/shared';
export function TradingConfigForm({ agent }: { agent: Agent }) {
const form = useForm({
resolver: zodResolver(TradingConfigSchema),
defaultValues: agent.tradingConfig,
});
const updateConfig = useUpdateAgentTradingConfig();
const onSubmit = (data: TradingConfig) => {
updateConfig.mutate({ agentId: agent.id, config: data });
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="positionSize.default"
render={({ field }) => (
<FormItem>
<FormLabel>Position Size (SOL)</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save</Button>
</form>
</Form>
);
}State Flow Summary
┌─────────────────────────────────────────────────────────────────┐
│ Component │
└─────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ React Query │ │ React Context │ │ WebSocket │
│ (Server State) │ │ (Client State) │ │ (Real-time) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ │ ▼
┌─────────────────┐ │ ┌─────────────────┐
│ API Client │ │ │ WS Connection │
└────────┬────────┘ │ └────────┬────────┘
│ │ │
▼ │ ▼
┌─────────────────────────────┴────────────────────────────────┐
│ Backend │
└───────────────────────────────────────────────────────────────┘