Skip to content

Commit e712d52

Browse files
committed
feat(frontend): Dual-layer caching with React Query + localStorage (#36)
- Add React Query for in-memory caching with deduplication - Add localStorage cache utility with TTL and versioning - Create useCachedQuery hooks (useDependencyGraph, useStyleAnalysis) - Cache invalidation on re-index via useInvalidateRepoCache - 5 min stale time, 30 min localStorage TTL - Instant tab switching (0 network calls) - Instant page refresh (localStorage hydration) Performance: ~100% reduction in redundant API calls
1 parent 078fc6c commit e712d52

8 files changed

Lines changed: 394 additions & 100 deletions

File tree

frontend/package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@radix-ui/react-progress": "^1.1.8",
1616
"@radix-ui/react-slot": "^1.2.4",
1717
"@supabase/supabase-js": "^2.39.0",
18+
"@tanstack/react-query": "^5.90.12",
1819
"@types/dagre": "^0.7.53",
1920
"@types/react-syntax-highlighter": "^15.5.13",
2021
"class-variance-authority": "^0.7.1",

frontend/src/components/DependencyGraph.tsx

Lines changed: 70 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ReactFlow, {
1111
} from 'reactflow'
1212
import dagre from 'dagre'
1313
import 'reactflow/dist/style.css'
14+
import { useDependencyGraph } from '../hooks/useCachedQuery'
1415

1516
interface DependencyGraphProps {
1617
repoId: string
@@ -47,17 +48,25 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
4748
export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps) {
4849
const [nodes, setNodes, onNodesChange] = useNodesState([])
4950
const [edges, setEdges, onEdgesChange] = useEdgesState([])
50-
const [loading, setLoading] = useState(true)
5151
const [metrics, setMetrics] = useState<any>(null)
5252
const [filterCritical, setFilterCritical] = useState(false)
5353
const [minDeps, setMinDeps] = useState(0)
5454
const [highlightedNode, setHighlightedNode] = useState<string | null>(null)
5555
const [allNodes, setAllNodes] = useState<Node[]>([])
5656
const [allEdges, setAllEdges] = useState<Edge[]>([])
5757

58+
// Use cached query for dependencies
59+
const { data, isLoading: loading, isFetching } = useDependencyGraph({
60+
repoId,
61+
apiKey
62+
})
63+
64+
// Process data when it arrives
5865
useEffect(() => {
59-
loadGraph()
60-
}, [repoId])
66+
if (data) {
67+
processGraphData(data)
68+
}
69+
}, [data])
6170

6271
useEffect(() => {
6372
if (allNodes.length > 0) {
@@ -89,83 +98,67 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps
8998
setEdges(layoutedEdges)
9099
}
91100

92-
const loadGraph = async () => {
93-
setLoading(true)
94-
try {
95-
const response = await fetch(`${apiUrl}/api/repos/${repoId}/dependencies`, {
96-
headers: {
97-
'Authorization': `Bearer ${apiKey}`
98-
}
99-
})
101+
const processGraphData = (data: any) => {
102+
const flowNodes: Node[] = data.nodes.map((node: any) => {
103+
const fileName = node.label || node.id.split('/').pop()
104+
const fullPath = node.id
105+
const importCount = node.import_count || node.imports || 0
100106

101-
const data = await response.json()
102-
103-
const flowNodes: Node[] = data.nodes.map((node: any) => {
104-
const fileName = node.label || node.id.split('/').pop()
105-
const fullPath = node.id
106-
const importCount = node.import_count || node.imports || 0
107-
108-
return {
109-
id: node.id,
110-
type: 'default',
111-
data: {
112-
label: (
113-
<div title={fullPath} style={{ cursor: 'pointer' }}>
114-
<div style={{ fontWeight: 600, fontSize: '11px', marginBottom: '4px' }}>
115-
{fileName}
116-
</div>
117-
{importCount > 0 && (
118-
<div style={{ fontSize: '9px', opacity: 0.8 }}>
119-
{importCount} imports
120-
</div>
121-
)}
107+
return {
108+
id: node.id,
109+
type: 'default',
110+
data: {
111+
label: (
112+
<div title={fullPath} style={{ cursor: 'pointer' }}>
113+
<div style={{ fontWeight: 600, fontSize: '11px', marginBottom: '4px' }}>
114+
{fileName}
122115
</div>
123-
),
124-
language: node.language,
125-
imports: importCount
126-
},
127-
position: { x: 0, y: 0 },
128-
style: {
129-
background: getLanguageColor(node.language),
130-
color: 'white',
131-
border: '2px solid #3b82f6',
132-
borderRadius: '8px',
133-
padding: '8px 12px',
134-
fontSize: '11px',
135-
fontFamily: 'monospace',
136-
width: 180,
137-
height: 60
138-
}
139-
}
140-
})
141-
142-
const flowEdges: Edge[] = data.edges.map((edge: any) => ({
143-
id: `${edge.source}-${edge.target}`,
144-
source: edge.source,
145-
target: edge.target,
146-
animated: false,
147-
style: { stroke: '#4b5563', strokeWidth: 1.5 },
148-
markerEnd: {
149-
type: MarkerType.ArrowClosed,
150-
color: '#4b5563',
116+
{importCount > 0 && (
117+
<div style={{ fontSize: '9px', opacity: 0.8 }}>
118+
{importCount} imports
119+
</div>
120+
)}
121+
</div>
122+
),
123+
language: node.language,
124+
imports: importCount
151125
},
152-
}))
153-
154-
setAllNodes(flowNodes)
155-
setAllEdges(flowEdges)
156-
setMetrics(data.metrics)
157-
158-
const { nodes: layoutedNodes, edges: layoutedEdges } =
159-
getLayoutedElements(flowNodes, flowEdges)
160-
161-
setNodes(layoutedNodes)
162-
setEdges(layoutedEdges)
163-
164-
} catch (error) {
165-
console.error('Error loading graph:', error)
166-
} finally {
167-
setLoading(false)
168-
}
126+
position: { x: 0, y: 0 },
127+
style: {
128+
background: getLanguageColor(node.language),
129+
color: 'white',
130+
border: '2px solid #3b82f6',
131+
borderRadius: '8px',
132+
padding: '8px 12px',
133+
fontSize: '11px',
134+
fontFamily: 'monospace',
135+
width: 180,
136+
height: 60
137+
}
138+
}
139+
})
140+
141+
const flowEdges: Edge[] = data.edges.map((edge: any) => ({
142+
id: `${edge.source}-${edge.target}`,
143+
source: edge.source,
144+
target: edge.target,
145+
animated: false,
146+
style: { stroke: '#4b5563', strokeWidth: 1.5 },
147+
markerEnd: {
148+
type: MarkerType.ArrowClosed,
149+
color: '#4b5563',
150+
},
151+
}))
152+
153+
setAllNodes(flowNodes)
154+
setAllEdges(flowEdges)
155+
setMetrics(data.metrics)
156+
157+
const { nodes: layoutedNodes, edges: layoutedEdges } =
158+
getLayoutedElements(flowNodes, flowEdges)
159+
160+
setNodes(layoutedNodes)
161+
setEdges(layoutedEdges)
169162
}
170163

171164
const handleNodeClick = useCallback((event: any, node: Node) => {

frontend/src/components/RepoOverview.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
33
import { Progress } from '@/components/ui/progress'
44
import type { Repository } from '../types'
55
import { WS_URL } from '../config/api'
6+
import { useInvalidateRepoCache } from '../hooks/useCachedQuery'
67

78
interface RepoOverviewProps {
89
repo: Repository
@@ -23,6 +24,9 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr
2324
const [progress, setProgress] = useState<IndexProgress | null>(null)
2425
const wsRef = useRef<WebSocket | null>(null)
2526
const completedRef = useRef(false)
27+
28+
// Cache invalidation hook
29+
const invalidateCache = useInvalidateRepoCache()
2630

2731
useEffect(() => {
2832
return () => {
@@ -62,6 +66,8 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr
6266
toast.success(`Indexing complete! ${data.total_functions} functions indexed.`, { id: 'reindex' })
6367
setIndexing(false)
6468
setProgress(null)
69+
// Invalidate caches after re-index
70+
invalidateCache(repo.id)
6571
onReindex()
6672
} else if (data.type === 'error') {
6773
completedRef.current = true

frontend/src/components/StyleInsights.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useStyleAnalysis } from '../hooks/useCachedQuery'
22

33
interface StyleInsightsProps {
44
repoId: string
@@ -7,27 +7,8 @@ interface StyleInsightsProps {
77
}
88

99
export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) {
10-
const [data, setData] = useState<any>(null)
11-
const [loading, setLoading] = useState(true)
12-
13-
useEffect(() => {
14-
loadStyleData()
15-
}, [repoId])
16-
17-
const loadStyleData = async () => {
18-
setLoading(true)
19-
try {
20-
const response = await fetch(`${apiUrl}/api/repos/${repoId}/style-analysis`, {
21-
headers: { 'Authorization': `Bearer ${apiKey}` }
22-
})
23-
const result = await response.json()
24-
setData(result)
25-
} catch (error) {
26-
console.error('Error loading style data:', error)
27-
} finally {
28-
setLoading(false)
29-
}
30-
}
10+
// Use cached query for style analysis
11+
const { data, isLoading: loading } = useStyleAnalysis({ repoId, apiKey })
3112

3213
if (loading) {
3314
return (

0 commit comments

Comments
 (0)