import React, { useState, useEffect, useCallback, useRef } from 'react'; import Map from './components/Map'; import Filters from './components/Filters'; import { DEFAULT_FILTERS } from './lib/constants'; const DEBOUNCE_MS = 150; export default function App() { const [filters, setFilters] = useState(DEFAULT_FILTERS); const [data, setData] = useState([]); const [resolution, setResolution] = useState(8); const [bounds, setBounds] = useState(null); const [loading, setLoading] = useState(false); const debounceRef = useRef(null); const abortControllerRef = useRef(null); // Debounced fetch when dependencies change useEffect(() => { if (!bounds) return; // Clear previous debounce timer if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(async () => { // Cancel any in-flight request if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); setLoading(true); try { const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`; const params = new URLSearchParams({ resolution: resolution.toString(), min_year: filters.minYear.toString(), max_year: filters.maxYear.toString(), min_price: filters.minPrice.toString(), max_price: filters.maxPrice.toString(), bounds: boundsStr, }); const res = await fetch(`/api/hexagons?${params}`, { signal: abortControllerRef.current.signal, }); const json = await res.json(); setData(json.features || []); } catch (err) { if (err.name !== 'AbortError') { console.error('Failed to fetch data:', err); } } finally { setLoading(false); } }, DEBOUNCE_MS); return () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, [filters, resolution, bounds]); const handleViewChange = useCallback(({ resolution: newRes, bounds: newBounds }) => { setResolution(newRes); setBounds(newBounds); }, []); return (