DEV Community

Cover image for Draw your custom polylines for directions on react-native-maps with Google Maps' Routes API and handle rerouting.
Victor Olufade
Victor Olufade

Posted on

Draw your custom polylines for directions on react-native-maps with Google Maps' Routes API and handle rerouting.

While working on a carpooling application over the past months using react-native, I had to use react-native-maps and react-native-maps-directions for map views and display of directions between locations.

Under the hood react-native-maps-directions used Google's directions api to display polylines on the map. By Mar 1, 2025, Google decided to move the Directions API to legacy status and this meant Google Map apiKeys obtained after this date would no longer have access to the Directions API.

Unfortunately I was utilizing a temporary apiKey before that date. By the time a billed key was provided, I could no longer utilize the react-native-maps-directions library. Google then replaced the Directions API with Routes API. To solve the problem, I had to write a custom hook that used the Routes API and the "@mapbox/polyline" library.

The implementation is shared here. This is not necessarily in the most optimized state. You can further customize as you deem fit. But this works and even has a rerouting logic integrated.

import React, { useState, useEffect, useRef } from "react";
import { Polyline, LatLng } from "react-native-maps";
import polyline from "@mapbox/polyline";
import { theme } from "@src/theme/theme";
import { scale } from "react-native-size-matters";
import { useDebounce } from "use-debounce";
import { convertSecondsToMinutes } from "@src/utils/general";
import { haversineDistance } from "@src/utils/maps";

interface MapWithRoutesProps {
  origin: LatLng;
  destination: LatLng;
  apiKey: string;
  strokeColor?: string;
  strokeWidth?: number;
  refetchLocation?: LatLng | undefined;
  updateRouteInfoThreshold?: number; // Distance threshold in meters to trigger reroute
  onRouteReady?: (coords: LatLng[]) => void;
}

interface RouteResponse {
  routes?: {
    polyline: {
      encodedPolyline: string;
    };
    distanceMeters: number;
    duration: string;
  }[];
}

const UseMapWithRoutes = ({
  origin,
  destination,
  apiKey,
  strokeColor = theme?.colors?.screens?.mapScreen?.directionsStroke,
  strokeWidth = scale(5),
  refetchLocation,
  updateRouteInfoThreshold = 100, // Default threshold of 50 meters
  onRouteReady,
}: MapWithRoutesProps) => {
  const [polylineCoordinates, setPolylineCoordinates] = useState<LatLng[]>([]);
  const [distanceTimeObj, setDistanceTimeObj] = useState({
    distanceMeters: 0 as number | undefined,
    duration: "",
  });

  const [justSetPolyline, setJustSetPolyline] = useState(false)
  const triggerSetPolyline = (bool: boolean) =>
    setJustSetPolyline((prev) => bool);

  const [refetchNow, setRefetchNow] = useState(false);
  const triggerImmediateRefetch = (bool: boolean) =>
    setRefetchNow((prev) => bool);

  const isRerouting = useRef(false);

  const checkDeviation = (
    currentLocation: LatLng,
    polylineCoords: LatLng[]
  ) => {
    if (!polylineCoords || polylineCoords.length === 0) return false;

    // Find the closest point on the polyline to the current location
    const closestPointDistance = Math.min(
      ...polylineCoords.map((point) =>
        haversineDistance(currentLocation, point)
      )
    );

    // Check if the distance exceeds the threshold
    return closestPointDistance > updateRouteInfoThreshold;
  };

  const handleReroute = async (currentLocation: LatLng) => {
    if (isRerouting.current) return; // Prevent multiple reroute requests
    isRerouting.current = true;

    try {
      await fetchRoutes(currentLocation, {
        latitude: destination?.latitude,
        longitude: destination?.longitude,
      });
    } finally {
      isRerouting.current = false;
    }
  };

  const fetchRoutes = async (
    start: LatLng,
    end: LatLng,
    refetchDistance?: boolean
  ) => {
    console.log("called fetchRoutes?>>>>>>>>>>>>>>>>>>");

    if (!start || !end) {
      return;
    }
    try {
      const response = await fetch(
        `https://routes.googleapis.com/directions/v2:computeRoutes?key=${apiKey}`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Goog-FieldMask":
              "routes.polyline.encodedPolyline,routes.distanceMeters,routes.duration",
          },
          body: JSON.stringify({
            origin: {
              location: {
                latLng: {
                  latitude: start.latitude,
                  longitude: start.longitude,
                },
              },
            },
            destination: {
              location: {
                latLng: {
                  latitude: end.latitude,
                  longitude: end.longitude,
                },
              },
            },
            travelMode: "DRIVE",
            routingPreference: "TRAFFIC_AWARE",
          }),
        }
      );

      const data: RouteResponse = await response.json();

      if (!data || !data?.routes) return;

      if (refetchDistance === true) {
        setDistanceTimeObj((prev) => ({
          ...prev,
          distanceMeters:
            data.routes![0].distanceMeters !== undefined
              ? Math.round(data.routes![0].distanceMeters / 1000)
              : 0,
          duration: convertSecondsToMinutes(data.routes![0].duration),
        }));
        return;
      }

      if (data.routes[0].polyline.encodedPolyline) {
        const encodedPolyline = data.routes[0].polyline.encodedPolyline;
        const decodedCoordinates = decodePolyline(encodedPolyline);
        setPolylineCoordinates(decodedCoordinates);
        triggerSetPolyline(true)
        data.routes && data.routes?.length > 0
          ? setDistanceTimeObj((prev) => ({
              ...prev,
              distanceMeters:
                data.routes![0].distanceMeters !== undefined
                  ? Math.round(data.routes![0].distanceMeters / 1000)
                  : undefined,
              duration: convertSecondsToMinutes(data.routes![0].duration),
            }))
          : null;
        if (onRouteReady) onRouteReady?.(decodedCoordinates);
        return decodedCoordinates;
      }
    } catch (error) {
      console.error("Error fetching routes:", error);
    }
  };

  useEffect(() => {
    if (
      destination?.latitude &&
      origin?.latitude &&
      (!polylineCoordinates || polylineCoordinates.length === 0)
    ) {
      fetchRoutes(
        { latitude: origin?.latitude, longitude: origin?.longitude },
        {
          latitude: destination?.latitude,
          longitude: destination?.longitude,
        }
      );
    }

    if (destination?.latitude && origin?.latitude && refetchNow === true) {
      triggerImmediateRefetch(false);
      fetchRoutes(
        { latitude: origin?.latitude, longitude: origin?.longitude },
        {
          latitude: destination?.latitude,
          longitude: destination?.longitude,
        }
      );
    }
  }, [
    destination?.latitude,
    origin?.latitude,
    polylineCoordinates?.length,
    refetchNow,
  ]);

  // Monitor driver location for deviation
  useEffect(() => { 
    if (
      refetchLocation?.latitude &&
      destination?.latitude &&
      polylineCoordinates?.length > 0
    ) {
      const hasDeviated = checkDeviation(refetchLocation, polylineCoordinates);
      if (hasDeviated) {
        handleReroute(refetchLocation);
      }
    }
  }, [refetchLocation?.latitude, polylineCoordinates?.length]);

  const decodePolyline = (encoded: string): LatLng[] => {
    return polyline
      .decode(encoded)
      .map((point: any) => ({ latitude: point[0], longitude: point[1] }));
  };

  const returnPolyLine = () => {
    if (polylineCoordinates && polylineCoordinates.length > 0) {
      return (
        <Polyline
          coordinates={polylineCoordinates}
          strokeColor={strokeColor}
          strokeWidth={strokeWidth}
        />
      );
    }
    return null;
  };

  return {
    fetchRoutes,
    returnPolyLine,
    triggerImmediateRefetch,
    triggerSetPolyline,
    justSetPolyline,
    distanceTimeObj,
  };
};

export default UseMapWithRoutes;

Enter fullscreen mode Exit fullscreen mode

You can then reuse the Component as shown here:

  const {
    returnPolyLine,
    distanceTimeObj,
    justSetPolyline,
    triggerSetPolyline,
  } = UseMapWithRoutes({
    origin: mapMarkers[0],
    destination: mapMarkers[1],
    apiKey: GOOGLE_MAPS_API_KEY,
    refetchLocation: defaultLocation,
    onRouteReady: handleMapDirectionsReady,
  });
Enter fullscreen mode Exit fullscreen mode

The returnPolyline function can then be used within your map view as in:

 <StyledMapView
            ref={models.mapRef}
            showsCompass={false}
            showsUserLocation={true}
            onUserLocationChange={operations.handleUserLocationChange}
            showsMyLocationButton={false}
            provider={PROVIDER_GOOGLE}
            customMapStyle={mapStyle}
            onRegionChangeComplete={operations.onregionChangeComplete}
          >
            {renderMapMarkers()}
            <MovingCarMarker
              ref={models?.carMarkerRef}
              defaultLocation={models?.defaultLocation}
            />
            {operations?.returnPolyLine()}
          </StyledMapView>
Enter fullscreen mode Exit fullscreen mode

I really hope this helps other developers who may be having a similar challenge.

Cheers!

Top comments (0)