import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { whenYmapsReady } from "../utils/ymapsApi"; const YANDEX_GEOCODER_API_KEY = process.env.REACT_APP_YANDEX_GEOCODER_KEY; const DEFAULT_CENTER = [55.751574, 37.573856]; export function useYandexMap(enabled, initialCoords = null, initialAddress = "") { const { t } = useTranslation(); // tRef читается из эффекта инициализации карты. Добавлять t в его deps нельзя: // смена языка тогда пересоздавала бы карту и теряла бы выбранную метку. const tRef = useRef(t); tRef.current = t; const mapRef = useRef(null); const ymapRef = useRef(null); const placemarkRef = useRef(null); const initializedRef = useRef(false); // защита от двойного вызова StrictMode const requestIdRef = useRef(0); // защита от гонки при быстрых кликах/драгах const pendingFocusRef = useRef(null); // фокус, который ждёт, пока карта инициализируется const [coords, setCoords] = useState(initialCoords); const [address, setAddress] = useState(initialAddress ?? ""); const addressRef = useRef(initialAddress ?? ""); const [loading, setLoading] = useState(false); // Ошибка загрузки API карт: без неё пользователь смотрел бы в вечный спиннер. const [loadError, setLoadError] = useState(""); // Всегда хранит последние координаты, чтобы при повторной инициализации // карты (например, при повторном открытии модалки) маркер появлялся там, // где пользователь его оставил, а не сбрасывался на initialCoords. const coordsRef = useRef(initialCoords); useEffect(() => { coordsRef.current = coords; }, [coords]); const resolveAddress = useCallback((lat, lng) => { const requestId = ++requestIdRef.current; setLoading(true); fetch( `https://geocode-maps.yandex.ru/1.x/?apikey=${YANDEX_GEOCODER_API_KEY}&geocode=${lng},${lat}&format=json&lang=ru_RU` ) .then(r => r.json()) .then(data => { if (requestId !== requestIdRef.current) return; const found = data?.response?.GeoObjectCollection?.featureMember?.[0] ?.GeoObject?.metaDataProperty?.GeocoderMetaData?.text; const resolved = found || `${lat.toFixed(5)}, ${lng.toFixed(5)}`; setAddress(resolved); addressRef.current = resolved; setLoading(false); }) .catch(() => { if (requestId !== requestIdRef.current) return; const fallback = `${lat.toFixed(5)}, ${lng.toFixed(5)}`; setAddress(fallback); addressRef.current = fallback; setLoading(false); }); }, []); const attachPlacemarkDrag = useCallback((map, pm) => { pm.events.add("dragend", () => { const [lat, lng] = pm.geometry.getCoordinates(); setCoords({ latitude: lat, longitude: lng }); resolveAddress(lat, lng); }); }, [resolveAddress]); /** * Устанавливает маркер и центр карты на указанные координаты. * Если карта ещё не инициализирована — откладывает фокус до её создания. */ const moveTo = useCallback((lat, lng) => { const map = ymapRef.current; if (!map) { pendingFocusRef.current = { lat, lng }; return; } pendingFocusRef.current = null; setCoords({ latitude: lat, longitude: lng }); if (placemarkRef.current) { placemarkRef.current.geometry.setCoordinates([lat, lng]); } else { const pm = new window.ymaps.Placemark([lat, lng], {}, { draggable: true }); map.geoObjects.add(pm); placemarkRef.current = pm; attachPlacemarkDrag(map, pm); } map.setCenter([lat, lng], 14); resolveAddress(lat, lng); }, [attachPlacemarkDrag, resolveAddress]); const init = useCallback(() => { if (ymapRef.current) return; const startCoords = coordsRef.current; const center = startCoords ? [startCoords.latitude, startCoords.longitude] : DEFAULT_CENTER; const map = new window.ymaps.Map(mapRef.current, { center, zoom: 14, controls: ["zoomControl", "geolocationControl"], }); ymapRef.current = map; if (startCoords) { const pm = new window.ymaps.Placemark(center, {}, { draggable: true }); map.geoObjects.add(pm); placemarkRef.current = pm; attachPlacemarkDrag(map, pm); resolveAddress(startCoords.latitude, startCoords.longitude); } map.events.add("click", e => { const [lat, lng] = e.get("coords"); moveTo(lat, lng); }); // Если геолокация успела вернуть координаты раньше, чем создалась карта, // применяем отложенный фокус сразу после инициализации. if (pendingFocusRef.current) { const pending = pendingFocusRef.current; pendingFocusRef.current = null; moveTo(pending.lat, pending.lng); } }, [attachPlacemarkDrag, resolveAddress, moveTo]); useEffect(() => { if (!enabled || !mapRef.current) return; if (initializedRef.current) return; // уже инициализировали let cancelled = false; // Сбрасываем прошлую ошибку на новой попытке, иначе после успешной // загрузки карты старая плашка об ошибке осталась бы на экране. setLoadError(""); whenYmapsReady() .then((ymaps) => { if (cancelled) return; initializedRef.current = true; ymaps.ready(init); }) .catch((e) => { if (cancelled) return; // API карт так и не подгрузился: сбрасываем флаг, чтобы следующая // попытка открытия карты её повторила, и гасим бесконечный спиннер. initializedRef.current = false; setLoadError(tRef.current("post.mapLoadFailed")); console.warn("Yandex Maps API failed to load:", e); }); return () => { cancelled = true; ymapRef.current?.destroy(); ymapRef.current = null; placemarkRef.current = null; initializedRef.current = false; pendingFocusRef.current = null; }; // enabled намеренно в deps — карта инициализируется/разрушается при открытии // и закрытии модалки. Начальные координаты читаются из coordsRef, чтобы // при повторном открытии маркер вставал на последний выбранный адрес. // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, init]); return { mapRef, coords, address, addressRef, loading, loadError, moveTo }; }