react-mapbox-gl icon indicating copy to clipboard operation
react-mapbox-gl copied to clipboard

Infinite loop when setting state in handleMoveEnd

Open boernard opened this issue 4 years ago • 6 comments

I have this component:

const Map = ReactMapboxGl({
  accessToken: process.env.MAPBOX_KEY
});

const MapView = () => {
  const [readMapCenter, setReadMapCenter] = useState([9.964037, 53.568269])

  const handleMoveEnd = (map) => {
    const curCenter = [map.getCenter().lng.toFixed(4), map.getCenter().lat.toFixed(4)]
    console.log(curCenter)
    //setReadMapCenter(curCenter) <== uncommenting this line creates infinite loop
  }

  return (
    <Map
      style="mapbox://styles/mapbox/streets-v9"
      center={[9.964037, 53.568269]}
      zoom={[12]}
      movingMethod="easeTo"
      containerStyle={{
        height: '100%',
        width: '100%'
      }}
      onMoveEnd={(map) => handleMoveEnd(map)}
    >
    </Map>
  )
}

With this code I see the mapCenter logged once to the console when I move the map. However when I uncomment the state setting in handleMoveEnd, the react app crashes with "Maxiumum update depth exceeded". Could somebody explain to me why this is happening?

boernard avatar Apr 13 '20 09:04 boernard

You can try https://github.com/welldone-software/why-did-you-render to debug

olso avatar May 07 '20 15:05 olso

I feel like this is something wrong with the lib, the onDragEnd works as expected...

if you set the centre after drag end, it doesn't cause the map to run the onDragEnd event, which means you dont get the infinite loop, however with onMoveEnd or onZoomEnd, setting the centre causes these events to fire, leading to infinite loop.

Are there any suggestions to fix this?

JClackett avatar Jul 30 '20 12:07 JClackett

I just encountered this issue. I believe it's due to onMoveEnd being setup in the componentdidmount, so when you call the useState method, it causes an endless loop of state change -> componentdidmount.

Anyway, on to my solution... YMMV. I used useRef.

const MapView = () => {
  //const [readMapCenter, setReadMapCenter] = useState([9.964037, 53.568269])
  const refMapCenter = useRef([9.964037, 53.568269]);

  const handleMoveEnd = (map) => {
    const curCenter = [map.getCenter().lng.toFixed(4), map.getCenter().lat.toFixed(4)]
    console.log(curCenter)
    refMapCenter.current = curCenter;
    //setReadMapCenter(curCenter) <== uncommenting this line creates infinite loop
  }

  return (
    <Map
      style="mapbox://styles/mapbox/streets-v9"
      center={[9.964037, 53.568269]}
      zoom={[12]}
      movingMethod="easeTo"
      containerStyle={{
        height: '100%',
        width: '100%'
      }}
      onMoveEnd={(map) => handleMoveEnd(map)}
    >
    </Map>
  )
}

dhysong avatar Sep 17 '20 17:09 dhysong

@dhysong but if you want to use the map with controlled center values, does setting the center to a ref cause the component re-render?

say you want to set the initial center based off some state and then keep it updated with the latest center after moving it?

const [center, setCenter] = React.useState( // some data fetch )

const setMapCenter = (map) => { // logic to set center }

<Map
    center={center}
    onMoveEnd={setMapCenter}
/>

im pretty sure you can't use refs to this??

JClackett avatar Sep 22 '20 11:09 JClackett

okay I found a way to get it working and that is to use refs + state

which means we can use the state in other parts of the app, but use the ref for the map and stops the loop

const mapCenter = React.useRef(props.initialCenter)
const [center, setCenter] = React.useState(props.initialCenter)

const setMapCenter = (map) => { 
    const newCenter = getNewCenterFromMap
    // this doesn't cause infinite loop anymore?
    setCenter(newCenter)
    mapCenter.current = newCenter
}

<Map
    center={mapCenter.current}
    onMoveEnd={setMapCenter}
/>

Edit: Okay this doesn't actually work, as if props.initialCenter changes, there's no way of updating the center without going back into the infinite loop

JClackett avatar Sep 22 '20 11:09 JClackett

Note: You can check whether onZoomEnd has been triggered by changed some Map property (contrary to being triggered by user interaction like dragging), by checking fitboundUpdate extra event data, like so:

  onZoomEnd={(_, event) => {
    if (event.fitboundUpdate) {
      console.log('Map bounds have been programmatically changed')
    } else {
      console.log('Map bounds have been changed by user interaction')
    }
  }}
/>

source: https://github.com/alex3165/react-mapbox-gl/blob/master/docs/API.md

rudza avatar Mar 17 '21 18:03 rudza