diff --git a/src/hooks/useLocations.ts b/src/hooks/useLocations.ts index 85ee1346..fe4f5dbc 100644 --- a/src/hooks/useLocations.ts +++ b/src/hooks/useLocations.ts @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useCallback } from "react" +import { useState, useEffect, useCallback, useRef } from "react" import { useClient, useCampaign } from "@/contexts/AppContext" import type { Location } from "@/types" @@ -34,70 +34,85 @@ export function useLocations(fightId: string | undefined): UseLocationsResult { const [locations, setLocations] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + // Track fight_id from websocket payloads to reliably filter cross-fight updates. + const latestWsFightId = useRef(fightId) - const fetchLocations = useCallback(async () => { - if (!fightId) { - setLocations([]) + useEffect(() => { + latestWsFightId.current = fightId + }, [fightId]) + + const fetchLocations = useCallback( + async (showLoading: boolean = true) => { + if (!fightId) { + setLocations([]) + setError(null) + setLoading(false) + return + } + + if (showLoading) { + setLoading(true) + } setError(null) - setLoading(false) - return - } - - setLoading(true) - setError(null) - - try { - const response = await client.getFightLocations(fightId) - setLocations(response.data.locations || []) - } catch (err) { - console.error("Error fetching locations:", err) - setError("Failed to load locations") - setLocations([]) - } finally { - setLoading(false) - } - }, [fightId, client]) + + try { + const response = await client.getFightLocations(fightId) + setLocations(response.data.locations || []) + } catch (err) { + console.error("Error fetching locations:", err) + setError("Failed to load locations") + setLocations([]) + } finally { + if (showLoading) { + setLoading(false) + } + } + }, + [fightId, client] + ) // Initial fetch useEffect(() => { fetchLocations() }, [fetchLocations]) - // Subscribe to WebSocket updates for locations - // The locations array contains location objects with fight_id, so we can filter directly + // Subscribe to fight_id updates for precise fight scoping. useEffect(() => { if (!fightId) return - const unsubscribe = subscribeToEntity("locations", (data: unknown) => { - // Data is an array of Location objects - if (Array.isArray(data)) { - // Filter to only include locations for this fight - // Each location has a fight_id property we can use for verification - const locationsData = data as Location[] - - // If locations array is empty, it might be for any fight - accept it - // If locations have fight_id, verify they're for this fight - const isForThisFight = - locationsData.length === 0 || - locationsData.some(loc => loc.fight_id === fightId) - - if (!isForThisFight) { - console.log( - "📍 [useLocations] Ignoring locations update for different fight" - ) - return - } + const unsubscribe = subscribeToEntity("fight_id", (wsFightId: unknown) => { + if (typeof wsFightId === "string") { + latestWsFightId.current = wsFightId + } + }) - console.log( - "📍 [useLocations] WebSocket locations update received for fight:", - fightId, - "with", - locationsData.length, - "locations" - ) - // Update locations without setting loading=true to avoid flicker - setLocations(locationsData) + return unsubscribe + }, [fightId, subscribeToEntity]) + + // Subscribe to WebSocket updates for locations. + // Contract: "locations" payload is always Location[]. + useEffect(() => { + if (!fightId) return + + const unsubscribe = subscribeToEntity("locations", (data: unknown) => { + if (!Array.isArray(data)) { + console.warn("[useLocations] Ignoring invalid locations payload", data) + return } + + const locationsData = data as Location[] + + const hasFightIds = locationsData.some(loc => !!loc.fight_id) + const isForThisFight = hasFightIds + ? locationsData.some(loc => loc.fight_id === fightId) + : locationsData.length === 0 + ? !latestWsFightId.current || latestWsFightId.current === fightId + : true + + if (!isForThisFight) return + + // Update locations without loading state changes to avoid UI flicker. + setLocations(locationsData) }) return unsubscribe