OS Trading Engine
Technical Documentation
Frontend
State Management

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

CategoryTechnologyExamples
Server StateReact QueryAgents, transactions, signals
Client StateReact ContextSelected agent, currency preference
Real-time StateWebSocketPositions, prices
Form StateReact Hook FormTrading 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:

PatternExample
['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

ContextPurpose
UserContextCurrent authenticated user
CurrencyContextUSD/SOL display preference, SOL price
TradingModeContextSimulation/live mode
WalletContextActive 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                              │
└───────────────────────────────────────────────────────────────┘