Files
nahodka-front/src/hooks/useYandexMap.js
2026-10-06 01:00:10 +03:00

177 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };
}