Master AMap Integration with Reusable React Hooks and Responsive Info Windows

This article details how to integrate Gaode (AMap) maps into a React frontend, covering script and npm imports, reusable map hooks, responsive marker info windows, event handling strategies, rendering abstractions for various map entities, and addressing common pitfalls such as marker overlap, bubble penetration, and memory leaks.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Master AMap Integration with Reusable React Hooks and Responsive Info Windows

Background

GuMing, a tea‑drink chain, needs to analyze store locations for profitability; visualizing site distribution, coverage, and competition using Gaode (AMap) maps provides a solution.

Import Methods

JS script import

<script src="https://webapi.amap.com/maps?v=2.0.0&key=YOUR_KEY"></script>
<script type="text/javascript">
  const map = new AMap.Map('container', {
    center: [117.000923, 36.675807],
    zoom: 11
  });
</script>

npm package import

import AMapLoader from '@amap/amap-jsapi-loader';
window._AMapSecurityConfig = { securityJsCode: 'YOUR_SECURITY_CODE' };
AMapLoader.load({
  key: '',
  version: '2.0',
  plugins: []
}).then(AMap => {
  const map = new AMap.Map('container');
}).catch(e => console.log(e));

AMap Capabilities

Map Instance Extraction

Map is used across multiple pages; extracting common capabilities improves reuse.

Various independent states (POI search, district boundaries, measuring tools, zoom level, view range, WebGL memory) need to be managed and cleaned up.

Encapsulation

Extract and encapsulate common basic capabilities for on‑demand use.

Expose map instance, zoom level, view bounds, measuring tool, mouse tool, POI search instance, district layer instance.

Internally destroy instances to avoid memory buildup.

import { debounce } from 'lodash';
import { useEffect, useState, useRef, useMemo } from 'react';
import { RangingTool } from '@/pages/map-mode/utils/index';
import { destroyMapWebGl, districtLayerPlugin } from '@/utils/AMapUtils';

const AMap = window.AMap;

export default (props) => {
  const { id, city, mapConfig = {} } = props;
  const defaultZoom = mapConfig.zoom || 10;
  const mapRef = useRef();
  const rangingToolRef = useRef();
  const mouseToolRef = useRef();
  const searchRef = useRef();
  const districtLayerPluginRef = useRef();
  const [currentZoom, setCurrentZoom] = useState(defaultZoom);
  const [currentBound, setCurrentBound] = useState(undefined);
  const [mapInstance, setMapInstance] = useState();

  const handleMapMove = useMemo(() => debounce(() => {
    const bound = mapRef.current.getBounds();
    setCurrentBound(bound);
  }, 50), []);

  useEffect(() => {
    if (!mapRef.current) {
      mapRef.current = new AMap.Map(id, mapConfig || {});
    }
    const map = mapRef.current;
    map.on('complete', () => {
      rangingToolRef.current = new RangingTool({ mapInstance: mapRef.current });
      mouseToolRef.current = new AMap.MouseTool(map);
    });
    map.on('zoomchange', () => {
      setCurrentZoom(mapRef.current.getZoom());
      setCurrentBound(mapRef.current.getBounds());
    });
    map.on('mapmove', () => handleMapMove());
    map.on('click', () => {});
    setMapInstance(map);
    return () => {
      const parentDom = document.getElementById(id);
      const canvas = parentDom?.getElementsByTagName('canvas');
      destroyMapWebGl(canvas);
      mapRef.current.destroy();
      mapRef.current = null;
      setMapInstance(null);
    };
  }, []);

  useEffect(() => {
    if (city && mapRef.current) {
      initMapSearch();
      mapRef.current.setCity(city);
      mapRef.current.setZoom(defaultZoom);
    }
  }, [city]);

  const initMapSearch = () => {
    searchRef.current = new AMap.PlaceSearch({
      city,
      citylimit: true,
      pageSize: 50,
      extensions: 'all'
    });
  };

  return {
    mapRef,
    mapInstance,
    rangingToolRef,
    mouseToolRef,
    searchRef,
    currentZoom,
    currentBound,
    districtLayerPluginRef,
  };
};

Usage

const createId = () => `${Math.random().toString(36).substr(2)}_${+new Date}`;
const [mapId] = useState(createId());
const { mapInstance, searchRef, currentZoom } = useInitMap({
  id: mapId,
  city,
  mapConfig: { zoom: 10, isHotspot: true }
});
return (<>
  <div id={mapId}></div>
</>);

Responsive Info Window

Background

Clicking a marker should open a popup that is tightly attached to the marker and responsive; using Marker.setLabel() or Marker.setContent() only accepts a string or DOM element, which limits component‑based development.

Problem

Cannot write marker content as a React component.

Popup content is static, not responsive.

Event binding is cumbersome, requiring manual DOM queries.

Form field values are hard to retrieve.

Solution

Insert a globally unique empty div into the marker content, render the React component into that div, and thus achieve a responsive, component‑based popup.
import ClueCreateDialog from "@c/omponents/ClueCreateDialog/index";
const uid = () => `${Math.random().toString(36).substr(2)}_${new Date().getTime()}`;
marker.setContent(`<div id="${uid}"></div>`);
mapInstance.add(marker);
ReactDom.render(<ClueCreateDialog />, document.getElementById(uid));

This approach resolves all listed issues.

Event Handling

Map Event Specificity

Map events and marker events are separate; binding them at creation can lock the current state, causing stale data in callbacks.

Both event types may need “penetration” control, which AMap does not support dynamically, leading to conflicts.

Two solutions:

Re‑bind events on state change (may affect performance for massive markers).

Keep initial bindings and use refs to store state, though refs hinder reactivity.

Comprehensive Trade‑off Solution

Map events are few; they can be dynamically re‑bound to always have the latest state.

Marker events are massive; bind once at initialization and use internal logic to decide execution.

Solid line = persistent event, dashed line = dynamic binding. Marker events cannot be dynamically toggled, so they remain bound; map events can be cleared when not needed.

Map Event Binding

Use use-map-events hook to automatically re‑bind events based on state.
export default (props) => {
  const { mapInstance, mapCurrentEventType, handleClickHotSpot, handleClickAddClue } = props;
  useEffect(() => {
    if (mapInstance) {
      mapInstance.clearEvents('hotspotclick');
      mapCurrentEventType === MAP_EVENTS.addGatherPoint && mapInstance.on('hotspotclick', handleClickHotSpot);
      mapInstance.clearEvents('click');
      mapCurrentEventType === MAP_EVENTS.addClue && mapInstance.on('click', handleClickAddClue);
    }
  }, [mapInstance, mapCurrentEventType]);
  return {};
};

Marker Event Binding

Bind once; inside the handler check a ref to decide whether to execute map‑level logic.
const handleClickPointMark = async (data) => {
  if (mapCurrentEventTypeRef.current) return;
  const { position } = data;
  mapRef.current && mapRef.current.panTo(position);
  setCurPointData(data);
  setShowPonitCard(true);
};

useRenderPointMarkers({
  pointsData: pointData,
  mapInstance,
  mapRef,
  handleClickPointMark,
});

Render Extraction

Entity Rendering Hooks

Point markers: use-render-point-markers Clue markers: use-render-clue-markers Competitor POI markers: use-render-poi-markers Planning polygons: use-render-plan-polygons Traffic lines: use-render-traffic-lines Each hook receives data and uses the appropriate AMap API to create the visual entity.

export default (props) => {
  const { pointsData = [], mapInstance, mapRef, handleClickPointMark } = props;
  const pointMarkersRef = useRef({});
  useEffect(() => {
    if (!mapInstance) return;
    Object.keys(pointMarkersRef.current).forEach(id => {
      mapRef.current.remove(pointMarkersRef.current[id]);
      delete pointMarkersRef.current[id];
    });
    pointsData?.forEach(item => {
      const size = POINT_MARKES_SIZE;
      const Icon = new AMap.Icon({
        size,
        anchor: POINT_MARKES_ANCHOR,
        image: item.iconConfig?.iconUrl,
        imageSize: size,
      });
      const marker = new AMap.Marker({
        position: new AMap.LngLat(item.position[0], item.position[1]),
        title: item.name,
        icon: Icon,
        extData: { item },
        visible: true,
        anchor: new AMap.Pixel(0, 0),
        offset: POINT_MARKES_OFFSET,
        bubble: true,
        zIndex: 999,
      });
      marker.on('click', () => handleClickPointMark?.(marker.getExtData().item));
      mapRef.current.add(marker);
      pointMarkersRef.current[item.id] = marker;
    });
  }, [pointsData, mapInstance]);
  return { pointMarkersRef };
};

Other Issues

AMap Defects

massMarker click penetration bug : Overlapping markers may register clicks on the lowest layer; this is a known bug.
Dynamic bubble property : Cannot change bubble at runtime; must set during event binding.

Marker Icon Offset

Different icons require proper anchor settings; using the correct anchor eliminates the need for manual offset adjustments.

Marker Drag Latitude/Longitude Offset

Using e.lnglat after dragging a non‑anchor point causes offset; the correct approach is e.target.getPosition() .
// Incorrect
markerRef.current.on('dragend', async e => {
  mapInstnce.setCenter([e.lnglat.lng, e?.lnglat.lat]);
});
// Correct
markerRef.current.on('dragend', async e => {
  mapInstnce.setCenter(e.target.getPosition());
});

Memory Leak

AMap’s destroy does not fully release WebGL resources, leading to memory growth when switching pages; the issue is acknowledged by AMap.

Workaround: lose WebGL context manually.

export const destroyMapWebGl = (canvass = []) => {
  for (let i = 0; i < canvass.length; i++) {
    const canvas = canvass[i];
    const gl = canvas.getContext('webgl');
    gl.getExtension('WEBGL_lose_context')?.loseContext?.();
  }
};

Summary

Extracting reusable map capabilities simplifies integration across business scenarios.

Responsive info windows are achieved by rendering React components into dynamically created DOM nodes.

Event handling balances performance and state freshness: map events are dynamically bound, marker events are bound once.

Separate hooks for each entity type promote modularity and reuse.

ReActevent handlingAmapMap HooksResponsive Info Window
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.