Deep Dive: React Query Data Architecture

The services layer integrates with React Query (TanStack Query) to create a predictable, efficient data layer. This deep-dive covers query keys, mutation strategies, optimistic updates, cache invalidation, and patterns for handling streaming responses and error boundaries.

🔑 Query Key Convention

Hierarchical keys: Structure query keys for effective invalidation.

// Convention: [module, resource, identifier, filters]
const queryKeys = {
  // Agents
  agentList: (page = 1) => ['agents', 'list', page],
  agentDetail: (id) => ['agents', 'detail', id],
  agentVersions: (id) => ['agents', 'versions', id],
  
  // Chats
  chatList: (limit = 20) => ['chats', 'list', limit],
  chatHistory: (chatId) => ['chats', 'history', chatId],
  
  // Knowledge
  knowledgeDatasets: () => ['knowledge', 'datasets'],
  knowledgeChunks: (datasetId, page = 1) => ['knowledge', 'chunks', datasetId, page],
  knowledgeSearch: (query, filters = {}) => ['knowledge', 'search', query, filters],
};

Benefits: Enables precise invalidation (e.g., invalidate all agent pages at once: invalidateQueries({ queryKey: ['agents'] }))

⚙️ Query & Mutation Setup

Query Pattern

// In service
export const useAgentList = (page: number) => {
    return useQuery({
        queryKey: queryKeys.agentList(page),
        queryFn: () => http.get('/api/v1/agents', { params: { page } }),
        staleTime: 5 * 60 * 1000,        // 5 min cache validity
        gcTime: 30 * 60 * 1000,          // 30 min before evict from memory
        refetchOnWindowFocus: true,      // Refetch when user returns to tab
        refetchOnReconnect: true,        // Refetch when network restored
        retry: 2,                        // Retry on failure
        throwOnError: false              // Don't throw; let component handle error state
    });
};

// In component
const { data, isLoading, error, refetch } = useAgentList(1);
if (isLoading) return ;
if (error) return ;
return ;

Mutation Pattern with Optimistic Update

export const useCreateAgentMutation = () => {
    const queryClient = useQueryClient();
    
    return useMutation({
        mutationFn: (newAgent: CreateAgentDTO) =>
            http.post('/api/v1/agents', newAgent),
        
        onMutate: async (newAgent) => {
            // Cancel in-flight queries to prevent race condition
            await queryClient.cancelQueries({ queryKey: ['agents'] });
            
            // Snapshot old data
            const previousAgents = queryClient.getQueryData(['agents', 'list', 1]);
            
            // Optimistically update cache
            queryClient.setQueryData(['agents', 'list', 1], old => 
                old ? [newAgent, ...old] : [newAgent]
            );
            
            return { previousAgents };
        },
        
        onError: (err, newAgent, context) => {
            // Rollback on error
            queryClient.setQueryData(['agents', 'list', 1], context.previousAgents);
            toast.error('Failed to create agent');
        },
        
        onSuccess: (createdAgent) => {
            // Invalidate to refetch fresh data from server
            queryClient.invalidateQueries({ queryKey: ['agents'] });
            toast.success('Agent created');
        }
    });
};

// In component
const mutation = useCreateAgentMutation();
const handleCreate = (formData) => {
    mutation.mutate(formData);
    // User sees new agent immediately (optimistic)
};

📡 Streaming & Server-Sent Events (SSE)

Challenge: React Query doesn't natively support streaming responses. Solution: Manual EventSource + query update.

export const useChatStreamMutation = () => {
    const queryClient = useQueryClient();
    
    return useMutation({
        mutationFn: async (message: string) => {
            return new Promise((resolve, reject) => {
                const eventSource = new EventSource(
                    `/api/v1/chats/stream?msg=${encodeURIComponent(message)}`
                );
                
                let fullResponse = '';
                
                eventSource.onmessage = (e) => {
                    const chunk = JSON.parse(e.data);
                    fullResponse += chunk.text;
                    
                    // Update message in cache
                    queryClient.setQueryData(['chat', 'history'], old => 
                        old.map(msg => 
                            msg.id === activeId 
                                ? { ...msg, content: fullResponse }
                                : msg
                        )
                    );
                };
                
                eventSource.onerror = () => {
                    eventSource.close();
                    reject(new Error('Stream failed'));
                };
                
                eventSource.onload = () => {
                    // Fires when stream ends
                };
            });
        },
        
        onSuccess: () => {
            queryClient.invalidateQueries(['chat', 'history']);
        }
    });
};

🔄 Cache Invalidation Strategies

1. Full Invalidation

// Invalidate all agents queries
queryClient.invalidateQueries({ queryKey: ['agents'] });

2. Selective Invalidation

// Invalidate only agent detail, not list
queryClient.invalidateQueries({ 
    queryKey: ['agents', 'detail', agentId] 
});

3. Manual Refetch

const { refetch } = useAgentList(1);
await refetch();

🌲 Related Queries & Cascading Updates

When updating agent triggers downstream invalidations:

const mutation = useMutation({
    mutationFn: (updates) => updateAgent(agentId, updates),
    onSuccess: () => {
        // Update detail
        queryClient.invalidateQueries(['agents', 'detail', agentId]);
        // Update list (agent now appears in list with new data)
        queryClient.invalidateQueries(['agents', 'list']);
        // Update related chats (if agent used by chats)
        queryClient.invalidateQueries(['chats']);
    }
});

🛡️ Error Handling & Boundaries

Query-Level Error

const { error } = useAgentList(1);
if (error?.response?.status === 404) {
    return ;
}
if (error?.response?.status === 401) {
    return ;
}

Mutation-Level Error with Retry

const mutation = useMutation({
    mutationFn: saveAgent,
    retry: (failureCount, error) => {
        // Retry max 3 times on network error
        if (error.code === 'NETWORK_ERROR') return failureCount < 3;
        return false;
    }
});

Error Boundary Component

export const QueryErrorBoundary = ({ children, fallback }) => {
    const { data: queries } = useQueries({
        queries: getActiveQueries() // Get all active queries
    });
    
    const hasError = queries.some(q => q.state.status === 'error');
    if (hasError) return fallback;
    return children;
};

💾 Persistence & Offline

Optional: Persist cache to localStorage for offline support.

import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';

const persistQueryClient = new PersistQueryClientProvider({
    client: queryClient,
    persistOptions: {
        persister: createSyncStoragePersister({
            storage: window.localStorage,
        }),
        maxAge: 1000 * 60 * 60 * 24, // 24 hours
        hydrateOptions: {
            defaultError: new Error('Offline'),
        },
    },
});

📊 Query Lifecycle Diagram

User Action
    ↓
Component calls useQuery/useMutation
    ↓
Is data in cache & not stale? → YES → Return cached data
    ↓ NO
Fetch from server
    ↓
Response arrives
    ↓
Update cache
    ↓
Component re-renders
    ↓
Time passes
    ↓
Data becomes stale (staleTime exceeded)
    ↓
On window focus / refetch trigger → Re-fetch in background

⚡ Performance Monitoring

React Query DevTools: Visualize queries in development.

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

export const App = () => {
    return (
        <>
            
            
        
    );
};

🐛 Debugging Tips

📚 Reference

Next Steps

{{TOPIC_NAME}}

Focus Deep-Dive

{{TOPIC_SUBTITLE}}

{{TOPIC_DESC}}

📚 What is this & why does it matter?

{{TOPIC_WHAT_WHY}} — Replace with 2-4 plain-English sentences explaining what this topic is and why a developer should understand it.

💡 Real-world analogy

{{TOPIC_ANALOGY}} — A vivid metaphor for this specific topic. 3-5 sentences.

❓ The Problem

{{THE_PROBLEM}} — 100+ words. What specific problem does this mechanism solve? What would happen without it? Paint a picture of the pain: "Without this, every request would..." or "The naive approach fails because..."

💡 The Approach

{{THE_APPROACH}} — 150+ words. How does this solution work, in narrative form? Walk through the key insight that makes the solution work. Don't just describe steps — explain WHY each step is necessary and what it contributes.

Contents

Overview

Primary role

Replace with the one-sentence job this mechanism performs inside the parent module.

Main inputs

Replace with the state, packets, structures, or conditions that feed this topic.

Key decisions

Replace with the most important branch or arbitration choices visible in the code.

Outputs / side effects

Replace with what changes after this mechanism runs: metadata, requests, counters, state, or responses.

flowchart TD
  A["Input / Trigger"] --> B["Phase 1"]
  B --> C["Phase 2"]
  C --> D["Phase 3"]
  D --> E["Output / Result"]

Replace with context-specific description of the overview diagram.

Decision Points

DecisionWhat controls itWhy it matters
Main branchCondition or mask from codeExplain how it changes the path or state
Fallback pathMiss / error / invalid state / resource pressureExplain the alternative behavior

How It Works

Detailed explanation of the mechanism, algorithm, or protocol. This is where the focus page adds value over the brief L2 mention. Anchor every paragraph to concrete symbols, tables, masks, counters, or states from the real code.

Phase/Step 1

Detailed explanation of the first phase. Include specific variable names, thresholds, or conditions from the source code.

// Replace with actual code from the source
void step1_example() {
    // ...
}
Key detail: Explain a non-obvious implementation detail visible in the code above.

Phase/Step 2

Continue with subsequent phases...

Behavior Diagram

stateDiagram-v2
    [*] --> Idle
    Idle --> Active : trigger
    Active --> Processing : start
    Processing --> Done : complete
    Processing --> Error : fail
    Error --> Idle : reset
    Done --> Idle : next

Replace with description of state transitions or message sequences specific to this topic.

Writing target: this section should explain dynamic behavior that a reader would otherwise have to reconstruct by reading several functions side by side.

Annotated Code Walkthrough

Walk through the actual source code step by step, with explanations between each snippet.

First, the entry point receives the input and validates it:

// Replace with real code snippet from source
void entry_point(Input& input) {
    if (!validate(input)) return;
    // ...
}

Then the core processing happens here — note the key decision point:

// Next real code snippet
Result process(Input& input) {
    // The critical branch:
    if (input.type == FAST_PATH) {
        return fast_process(input);
    }
    return slow_process(input);
}
Performance note: The fast path avoids X overhead. The slow path is taken in ~Y% of cases.

Edge Cases & Error Handling

ScenarioBehaviorCode Location
Edge case 1What happensfile.cc:123
Edge case 2What happensfile.cc:456

🔄 Why Not Alternatives?

Understanding why the current approach was chosen requires seeing what was rejected:

AlternativeHow It WorksWhy It Was RejectedWhen It Would Be Better
Alternative ABrief descriptionSpecific reason: performance, complexity, correctnessUnder what conditions this would actually be the right choice
Alternative BBrief descriptionSpecific reasonWhen it would win

⚖️ Complexity & Tradeoffs

What This Approach Costs

DimensionCostBenefit
Time complexityO(?)What this buys
Space complexityO(?)What this enables
Code complexityLines / indirectionWhat this simplifies elsewhere
Maintenance burdenWhat's hard to changeWhat's easy to extend

The honest truth: {{HONEST_ASSESSMENT}} — 50+ words. Where does this approach struggle? What would you change with unlimited time? What technical debt exists?

🚨 Failure Recovery

Failure ScenarioDetectionRecovery StrategyData Loss Risk
Crash mid-operationHow detectedHow the system recovers🟢 None / 🔴 Possible
Invalid inputValidation checkError path🟢 None

Configuration

Knob / ParameterDefaultDescription
param_1valueWhat it controls and how it affects this topic
param_2valueDescription

See also:

Directory Map

Source files for this topic
submod_name/
├── relevant_file_a.ext        ← Role: brief annotation
├── relevant_file_b.ext        ← Role: brief annotation
└── relevant_file_c.ext        ← Role: brief annotation