visgl / visgl/react-map-gl

[Bug] Marker beaving weirdly when i try to drag it with maxBounds defined and the map reached maxBounds

Open
#2,540 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
8.5k
Forks
1.4k
Avg merge
5d 17h
Merged PRs (30d)
3

Description

Description

When i try to move my marker, it behaves normally, but when i zoom out so i reach the maxBounds, either on top/left/right/bottom, and then, when i try to move my marker, it jumps randomly across the map

video (showing how my marker behaves normally, but when i reach the edge of the map and try to move it, bug starts to occur):

https://github.com/user-attachments/assets/b3f76ff2-cd44-4fca-adbb-30f94de45b6f

If needed, i can add the component code here

Expected Behavior

Marker should not behave differently when the map is at the edge of the bounds

Steps to Reproduce

Here's my component code, maybe it will be useful:

import { useState, useCallback, useRef, useEffect } from 'react';
import Map, {
  NavigationControl,
  GeolocateControl,
  Marker,
} from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
import { MapFill } from '../styles';
import { useData } from '@/context/DataContext';
import {
  LngLatBoundsLike,
} from 'mapbox-gl';
import EntityDetailsPanel from '@/components/Mapbox/EntityDetailsPanel';
import GeoJsonDataProcessor from './GeoJsonDataProcessor';
import PieChartImageGenerator from './PieChartImageGenerator';

interface MapComponentProps {
  searchMarker?: { longitude: number; latitude: number } | null;
  searchingViaInput?: { longitude: number; latitude: number } | null;
  onMarkerDrag?: (position: { longitude: number; latitude: number }) => void;
  radiusSize: number;
  isDragEnabled: boolean;
  isRadiusVisible: boolean;
}

export default function MapComponent({
  searchMarker,
  searchingViaInput,
  onMarkerDrag,
  radiusSize,
  isDragEnabled,
  isRadiusVisible,
}: MapComponentProps) {
  const [viewState, setViewState] = useState({
    longitude: 15.9819,
    latitude: 44.915,
    zoom: 7,
  });
  const [markerPosition, setMarkerPosition] = useState(
    searchMarker || { longitude: 0, latitude: 0 }
  );
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [hoveredId, setHoveredId] = useState<string | null>(null);
  const [mapLoaded, setMapLoaded] = useState(false);
  const [activeEntity, setActiveEntity] = useState<any>(null);
  const activeEntityIdRef = useRef<string | null>(null);
  const mapRef = useRef<any>(null);
  const { entities } = useData();
  const { data: geoJson, loading, error } = entities;

  const mapBounds: LngLatBoundsLike = [
    [-7, 37],
    [37.8, 52],
  ];

  // Handle map click
  const handleMapClick = useCallback((e: any) => {
    if (!mapRef.current) return;

    try {
      const layers = mapRef.current.getStyle().layers || [];
      const layerExists = layers.some(
        (l: any) =>
          l.id === 'unselected-entities' || l.id === 'selected-entities'
      );

      if (!layerExists) return;

      const features = mapRef.current.queryRenderedFeatures(e.point, {
        layers: ['unselected-entities', 'selected-entities'],
      });

      if (features.length > 0) {
        const clickedFeature = features[0];
        const id = clickedFeature.properties?.id;

        if (activeEntityIdRef.current === id) {
          return;
        }

        setSelectedId(id);
        activeEntityIdRef.current = id;

        const cleanProperties = {
          ...clickedFeature.properties,
          kategorije: clickedFeature.properties.kategorije
            ? JSON.parse(clickedFeature.properties.kategorije)
            : [],
          mainCats: clickedFeature.properties.mainCats
            ? JSON.parse(clickedFeature.properties.mainCats)
            : [],
          groupedCategories: clickedFeature.properties.groupedCategories
            ? JSON.parse(clickedFeature.properties.groupedCategories)
            : [],
        };

        setActiveEntity(cleanProperties);
      } else {
        setSelectedId(null);
        activeEntityIdRef.current = null;
        setActiveEntity(null);
      }
    } catch (error) {
      console.warn('Error querying features:', error);
    }
  }, []);

  // Handle mouse move
  const handleMouseMove = useCallback((e: any) => {
    if (!mapRef.current) return;

    try {
      const layers = mapRef.current.getStyle().layers || [];
      const layerExists = layers.some(
        (l: any) =>
          l.id === 'unselected-entities' || l.id === 'selected-entities'
      );

      if (!layerExists) return;

      const features = mapRef.current.queryRenderedFeatures(e.point, {
        layers: ['unselected-entities', 'selected-entities'],
      });

      if (features.length > 0) {
        setHoveredId(features[0].properties?.id);
        mapRef.current.getCanvas().style.cursor = 'pointer';
      } else {
        setHoveredId(null);
        mapRef.current.getCanvas().style.cursor = '';
      }
    } catch (error) {
      console.warn('Error querying features:', error);
    }
  }, []);

  // Handle mouse leave
  // const handleMouseLeave = useCallback(() => {
  //   setHoveredId(null);
  //   if (mapRef.current) {
  //     mapRef.current.getCanvas().style.cursor = '';
  //   }
  // }, []);

  // Handle marker drag
  const handleDragEnd = (event: any) => {
    const newPosition = {
      longitude: event.lngLat.lng,
      latitude: event.lngLat.lat,
    };
// IF I DISABLE setMarkerPosition PROBLEM DOESN'T OCCUR, BUT I INTRODUCE NEW PROBLEMS
    setMarkerPosition(newPosition);
    if (onMarkerDrag) onMarkerDrag(newPosition);
  };

  // Fly to location when searching via input
  useEffect(() => {

    if (!searchingViaInput || !mapLoaded || !mapRef.current) return;
    mapRef.current.flyTo({
      center: [searchingViaInput.longitude, searchingViaInput.latitude],
      zoom: 9,
      duration: 300,
    });
  }, [searchingViaInput, mapLoaded]);

  // Update marker position on search
  useEffect(() => {
    if (searchMarker) {
      setMarkerPosition(searchMarker);
    }
  }, [searchMarker]);



  const token = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;
  if (!token) return <p>No Mapbox token.</p>;
  if (loading) return <p>Loading entities…</p>;
  if (error) return <p>Error: {String(error)}</p>;
  if (!geoJson) return <p>No data.</p>;

  return (
    <MapFill>
      {activeEntity && (
        <EntityDetailsPanel
          entity={activeEntity}
          onClose={() => {
            setSelectedId(null);
            activeEntityIdRef.current = null;
            setActiveEntity(null);
          }}
        />
      )}
      <Map
        ref={mapRef}
        mapboxAccessToken={token}
        mapStyle="mapbox://styles/mapbox/streets-v12"
        {...viewState}
         onMove={(ev) => setViewState(ev.viewState)}
         onClick={handleMapClick}
         onMouseMove={handleMouseMove}
         dragRotate={false}
        minZoom={6.2}
        maxZoom={18}
          maxBounds={mapBounds} //When bounds are set, on map bounds, marker breaks
        // onMouseLeave={handleMouseLeave} //not using
        onLoad={() => setMapLoaded(true)}
        interactiveLayerIds={['unselected-entities', 'selected-entities']}
      >
        <NavigationControl position="top-right" />
        <GeolocateControl position="top-right" trackUserLocation />
        <GeoJsonDataProcessor
          geoJson={geoJson}
          selectedId={selectedId}
          hoveredId={hoveredId}
          markerPosition={markerPosition}
          radiusSize={radiusSize}
          isRadiusVisible={isRadiusVisible}
        />
        <PieChartImageGenerator
          mapRef={mapRef}
          mapLoaded={mapLoaded}
          geoJson={geoJson}
        />
        {markerPosition && (
          <Marker
            longitude={markerPosition.longitude}
            latitude={markerPosition.latitude}
            color="red"
            style={{ cursor: isDragEnabled?"pointer":"" }}
            draggable={isDragEnabled}
            onDragEnd={handleDragEnd}
          />
        )}
      </Map>
    </MapFill>
  );
}
Environment
  • Framework version: "react-map-gl": "^8.0.4",

  • Map library: "mapbox-gl": "^3.12.0",

  • Browser: all browsers

  • OS: windows

Logs

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the provided React component with react-map-gl 8.0.4 and mapbox-gl 3.12.0, focusing on maxBounds, a draggable Marker, and the setMarkerPosition update in handleDragEnd. Trace the marker drag behavior when the map reaches each bound; done means dragging no longer causes the marker to jump while bounds remain enforced.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.