1

I have the following code which renders a React Leaflet v3 map and allows users to construct a route by clicking at various points on the map. The total route distance is displayed below the map.

interface RouteProps {
  updateDistance: (d: number) => void;
}

function Route(props: RouteProps) {
  const initialRoute: [number, number][] = [];
  const [route, setRoute] = useState(initialRoute);

  const calculateDistance = (point1: LatLng, point2: LatLng): number => {
    // Omitted for brevity - it returns the distance between point1 and point2
  }


  const map = useMapEvents({
    click(e: any) {
      setRoute((prevValue) => {
        let lastPoint = null;
        if (prevValue.length > 0) {
            lastPoint = prevValue[prevValue.length - 1];
            const dist = calculateDistance(new LatLng(lastPoint[0], lastPoint[1]), new LatLng(e.latlng.lat, e.latlng.lng));
            console.log('marker added, calling updateDistance', dist)
            props.updateDistance(dist);
        }
        return [...prevValue, [e.latlng.lat, e.latlng.lng]];
      });
      map.panTo(e.latlng);
    }
  });

  return (
      <React.Fragment>
          {route.map((point, idx) => <Marker key={idx} position={new LatLng(point[0], point[1])}></Marker>)}
          <Polyline pathOptions={{fillColor: 'blue'}} positions={route} />
      </React.Fragment>
  );
}

function App() {

  const [distance, setDistance] = useState(0);
  const updateDistance = (d: number) => {
      console.log('update distance called', d)
      setDistance((prevValue) => { 
        console.log('setting new distance', d);
        return  prevValue + d
      });
  };

  return (
    <React.Fragment>
      <MapContainer
        center={[55, -2]}
        zoom={12}
        scrollWheelZoom={false}
        style={{ width: "100vw", height: "80vh" }}
      >
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <Route updateDistance={updateDistance} />
      </MapContainer>
      <h6>Distance: {distance.toFixed(2)} miles</h6>
     </React.Fragment>
  );
}

A StackBlitz of this can be found here. Please note the markers don't render properly on the map, but that shouldn't matter for the purposes of the demo.

With this approach, I define distance in the top level component using useState and pass a handler that updates the total distance to the Route component:

const updateDistance = (d: number) => {
  console.log('update distance called', d)
  setDistance((prevValue) => { 
    console.log('setting new distance', d);
    return  prevValue + d
  });
};

<MapContainer ...>
  ...
  <Route updateDistance={updateDistance} />
</MapContainer>

The click handler defined within useMapEvents updates the route state (which exists on in the Route component), and also calls the updateDistance function passed in via props:

const map = useMapEvents({
  click(e: any) {
    setRoute((prevValue) => {
      let lastPoint = null;
      if (prevValue.length > 0) {
        lastPoint = prevValue[prevValue.length - 1];
        const dist = calculateDistance(new LatLng(lastPoint[0], lastPoint[1]), new LatLng(e.latlng.lat, e.latlng.lng));
        console.log('marker added, calling updateDistance', dist)
        props.updateDistance(dist);
      }
      return [...prevValue, [e.latlng.lat, e.latlng.lng]];
    });
    map.panTo(e.latlng);
  }
});

When points are added to the route, I can see updateDistance (and therefore setDistance) are getting called twice. Open the console in the above StackBlitz to see the output.

I can "fix" the issue by defining the route state in the top level component and passing it through to the Route component as can be seen in this StackBlitz. Although this works, my rationale behind defining the route state within the Route component and the distance state in the top level component, is that the top level component only needs to know about the distance and not details of the actual route.

I'm trying to understand why the first StackBlitz behaves like it does. I don't believe I'm mutating state anywhere and am always returning a new value for the both the route and distance state, and I'm also using the functional version of setState where a state update needs to use the value of the previous state.

Ian A
  • 5,622
  • 2
  • 22
  • 31
  • Does [this](https://stackoverflow.com/a/69898741/14207720) answer your question? – Andrey Nov 24 '21 at 20:41
  • On my local dev environment, removing `` made the problem go away, however from what I've read in the discussion [here](https://github.com/facebook/react/issues/12856), I don't believe that's a good fix as it's pointing to a problem in my code with side effects. The strange thing is that when I created the StackBlitz example, I'm not using strict mode there and I still see the same behaviour as in my local environment with strict mode enabled. – Ian A Nov 25 '21 at 09:58

0 Answers0