react-google-mapsreact-google-maps-api

React google maps api - DirectionsRenderer isnt removed from the map


The main idea is im showing several routes on a map,

for each route i render a <DirectionsRenderer component.

however, when i try to remove a DirectionsRenderer it doesn't actually remove it from the map.

what i noticed is that on every render, it renders a couple one on top of the other, so when i remove one, only the top one is removed.

Map component code:

const RoutesResultMap: FC<Props> = ({}) => {
    const { GoogleMapProps, rendererProps, mapDivRef, map } = useMap();

    return (
        <>
            <div style={{ height: '90%' }} ref={mapDivRef}>
                <MapType2 GoogleMapProps={GoogleMapProps}>
                    {rendererProps.map((rendererProp, i) => {
                        return <DirectionsRenderer {...rendererProp} />;
                    })}
                </MapType2>
            </div>
        </>
    );
};

Directions renderer logic - TLDR: Basically creating a (DirectionsRendererProps type) for each route, using a promises array..

const useSimulationDirections = (selectedRoutes: IOptimumRouteData[]) => {
    const dispatch = useAppDispatch();

    const [rendererProps, setRendererProps] = useState<DirectionsRendererProps[]>([]);

    const fetchDirections = () => {
        return new Promise((resolve) => {
            try {
                const requests: { request: google.maps.DirectionsRequest; color: string }[] = [];

                const rendererPropsCopy: DirectionsRendererProps[] = [];

                const directionsService = new google.maps.DirectionsService();

                selectedRoutes.forEach((currRoute) => {
                    const isCoordsExistOnAllWaypoints = isAllObjectsContainsCoords(currRoute.optimumRoute);

                    if (isCoordsExistOnAllWaypoints && currRoute.optimumRoute.length > 1) {
                        const routeLength = currRoute.optimumRoute.length;

                        const origin = {
                            lat: currRoute.optimumRoute[0].lat,
                            lng: currRoute.optimumRoute[0].lng,
                        };

                        const destination = {
                            lat: currRoute.optimumRoute[routeLength - 1].lat,
                            lng: currRoute.optimumRoute[routeLength - 1].lng,
                        };

                        const stopWaypoints = currRoute.optimumRoute.slice(1, -1);

                        const waypoints = stopWaypoints.map((waypoint): google.maps.DirectionsWaypoint => {
                            // todo - name
                            return {
                                location: { lat: waypoint.lat, lng: waypoint.lng },
                                stopover: true,
                            };
                        });

                        const request: google.maps.DirectionsRequest = {
                            origin,
                            destination,
                            waypoints,
                            travelMode: google.maps.TravelMode.DRIVING,
                            avoidTolls: true,
                            drivingOptions: {
                                departureTime,
                                trafficModel: google.maps.TrafficModel.OPTIMISTIC,
                            },
                        };

                        requests.push({ request, color: getColorByRoute(currRoute, []) });
                    }
                });

                const serviceResponseCb: ServiceResponseCallBack = (response, status, color) => {
                    if (status === 'OK' && response) {
                        rendererPropsCopy.push({
                            directions: response,
                            options: {
                                suppressMarkers: true,
                                preserveViewport: true,
                                polylineOptions: {
                                    strokeColor: color ? color : '#28c2ff',
                                    strokeOpacity: 0.7,
                                    strokeWeight: 7,
                                },
                            },
                        });
                    }
                };

                const promises: Promise<google.maps.DirectionsResult>[] = [];

                requests.forEach((request) => {
                    const responsePromise = directionsService.route(request.request, (response, status) =>
                        serviceResponseCb(response, status, request.color)
                    );
                    promises.push(responsePromise);
                });

                Promise.allSettled(promises).then(() => {
                    setRendererProps(rendererPropsCopy);
                });
            } catch (error) {
                console.error(error);
                setRendererProps([]);
                //  resolve with -1 to indicate error
                resolve(-1);
            }
        });
    };

    useEffect(() => {
        if (selectedRoutes.length) {
            // reset rendererProps
            fetchDirections();
        } else {
            setRendererProps([]);
        }
    }, [selectedRoutes.length]);

    return { rendererProps };
};


Solution

  • SOLVED

    Code:

    type DirectionsRendererItemsRef = React.MutableRefObject<
        {
            renderer: google.maps.DirectionsRenderer;
            id: string;
        }[]
    >;
    
    const DirectionsRendererComponent = ({
        rendererProp,
        refs,
    }: {
        rendererProp: DirectionsRendererProps;
        refs: DirectionsRendererItemsRef;
    }) => {
        const [id] = useState(uuid());
    
        const onLoad: (directionsRenderer: google.maps.DirectionsRenderer) => void = (directionsRenderer) => {
            // Save the directionsRenderer instance in the ref
            refs.current.push({ renderer: directionsRenderer, id });
        };
    
        useEffect(() => {
            return () => {
                refs.current.forEach((ref) => {
                    if (ref.id === id) {
                        ref.renderer.setMap(null);
                    }
                });
            };
        }, []);
    
        return <DirectionsRenderer {...rendererProp} onLoad={onLoad} />;
    };
    
    const RoutesRenderers: React.FC<{ rendererProps: DirectionsRendererProps[] }> = ({ rendererProps }) => {
        const refs: DirectionsRendererItemsRef = useRef([]);
    
        return (
            <>
                {rendererProps.map((rendererProp, i) => {
                    return <DirectionsRendererComponent rendererProp={rendererProp} key={i} refs={refs} />;
                })}
            </>
        );
    };
    

    Explanation:

    The issue I encountered seems to have stemmed from the way the @react-google-maps/api library manages the lifecycle of the DirectionsRenderer component. Specifically, it looks like the library isn't effectively removing the underlying Google Maps DirectionsRenderer instance from the map when the DirectionsRenderer React component is unmounted.

    React uses a diffing algorithm to handle the addition and removal of components based on changes in state, props, and the unique key prop. In my situation, even though the DirectionsRenderer components were being correctly removed and added according to state changes, the underlying Google Maps DirectionsRenderer instances weren't being properly cleaned up. This appears to be a result of the library not implementing the required cleanup in the componentWillUnmount lifecycle method or the useEffect cleanup function for functional components.

    To solve this problem, I implemented a manual cleanup process. I stored a reference to each DirectionsRenderer instance and then, when the component was unmounted, I invoked setMap(null) on each instance. This effectively removed the DirectionsRenderer from the map, addressing the issue I was facing.