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;
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,
});
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>
I really hope this helps other developers who may be having a similar challenge.
Cheers!
Top comments (0)