Replace with the one-sentence job this mechanism performs inside the parent module.
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
- Check queryKey: Typo in key = cache miss
- Check staleTime: Data too fresh = no refetch
- Check gcTime: Evicted from cache too early?
- Log queryClient state:
console.log(queryClient.getQueryData(['agents'])) - Use DevTools: Visual inspect all queries + mutations
📚 Reference
- React Query Docs: https://tanstack.com/query/latest/docs/overview
- Query Key Factory: https://tanstack.com/query/latest/docs/react/guides/important-defaults#using-the-querykeyfactory-pattern
- Server State vs Client State: https://tanstack.com/query/latest/docs/react/guides/important-defaults#dont-treat-the-querykey-as-a-dependency-array
Next Steps
- Implement query key factory for your domain (agents, chats, etc.)
- Set up React Query DevTools in dev environment
- Add error boundaries around major features
- Back to Services Module
{{TOPIC_NAME}}
{{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
Replace with the state, packets, structures, or conditions that feed this topic.
Replace with the most important branch or arbitration choices visible in the code.
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
| Decision | What controls it | Why it matters |
|---|---|---|
| Main branch | Condition or mask from code | Explain how it changes the path or state |
| Fallback path | Miss / error / invalid state / resource pressure | Explain 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() {
// ...
}
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.
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);
}
Edge Cases & Error Handling
| Scenario | Behavior | Code Location |
|---|---|---|
| Edge case 1 | What happens | file.cc:123 |
| Edge case 2 | What happens | file.cc:456 |
🔄 Why Not Alternatives?
Understanding why the current approach was chosen requires seeing what was rejected:
| Alternative | How It Works | Why It Was Rejected | When It Would Be Better |
|---|---|---|---|
| Alternative A | Brief description | Specific reason: performance, complexity, correctness | Under what conditions this would actually be the right choice |
| Alternative B | Brief description | Specific reason | When it would win |
⚖️ Complexity & Tradeoffs
What This Approach Costs
| Dimension | Cost | Benefit |
|---|---|---|
| Time complexity | O(?) | What this buys |
| Space complexity | O(?) | What this enables |
| Code complexity | Lines / indirection | What this simplifies elsewhere |
| Maintenance burden | What's hard to change | What'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 Scenario | Detection | Recovery Strategy | Data Loss Risk |
|---|---|---|---|
| Crash mid-operation | How detected | How the system recovers | 🟢 None / 🔴 Possible |
| Invalid input | Validation check | Error path | 🟢 None |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
param_1 | value | What it controls and how it affects this topic |
param_2 | value | Description |
Related Topics
See also:
- {{SUBMOD_NAME}} overview — the parent L2 page
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