React Native Taxi App: Drawing a Route.
Tags: reactnative, react, testing, googlecloud
Published: 2021-09-11 12:02:33

Previously we added the basic logic for the Booking Information. We displayed a modal where the user can enter the destination address and using Places API from Google. We show an array of predictions using the FlatList component from React Native.

When the user presses one of the predictions, we will draw a route using Polygon and other handy features, so let's dive into it.

## Dispatching Destination Place

We need to create a dispatch action for setting the destination place when we press one of the predictions. Remember that we use a Places Manager Context Provider. Let's open it `src/context/PlacesManager.js`:

```js
export const placeReducer = (prevState, action) => {
  switch (action.type) {
    case 'SET_CURRENT_PLACE':
      ...
    
 👉 case 'SET_DESTINATION_PLACE':
      return {
        ...prevState,
        destinationPlace: {
          description: action.description,
          placeId: action.placeId,
        },
      };
  }
};
```

We update the `destinationPlace` object with the prediction `description` and `placeId` that the user select.

Now, let's move into our **Prediction** component (`src/components/Prediction.js`) and use `dispatchPlace` function from our Places Manager Context Provider.

```js
import React from 'react';
import {TouchableOpacity} from 'react-native';
import styled from 'styled-components/native';
import {usePlace} from '../context/PlacesManager'; 👈

const Text = styled.Text`
  padding: 5px;
  font-size: 14px;
`;

export default function Prediction({description, place_id}) {
  const {dispatchPlace} = usePlace(); 👈
  return (
    <TouchableOpacity
      key={place_id}
      testID={`prediction-row-${place_id}`}
      onPress={() => {
     👉 dispatchPlace({
          type: 'SET_DESTINATION_PLACE',
          description,
          placeId: place_id,
        });
      }}>
      <Text>{description}</Text>
    </TouchableOpacity>
  );
}

```
We need to import the `usePlace` hook into the **Prediction** component so the user can select a prediction and update the `destinationPlace` object using the `dispatchPlace` function.

Once we select a prediction, we update the destinationPlace and also we need to close the modal. For that reason let's pass the `toggleModal` function prop into our **Predictions** component. Open **SearchAddressModal** component

```js
 const renderPredictions = ({item}) => (
    <Prediction {...item} toggleModal={toggleModal} />
  );
```
And **Predictions** component.
```js

export default function Prediction({
description, 
place_id, 
toggleModal 👈
}) {
  const {dispatchPlace} = usePlace();
  return (
    <TouchableOpacity
      key={place_id}
      testID={`prediction-row-${place_id}`}
      onPress={() => {
        dispatchPlace({
          type: 'SET_DESTINATION_PLACE',
          description,
          placeId: place_id,
        });
        toggleModal(); 👈
      }}>
      <Text>{description}</Text>
    </TouchableOpacity>
  );
}
```
![Select a destination place](https://media.giphy.com/media/XYB31S2Nl4zJQbK42J/giphy.gif)

If everything is okay, you should see the selected Destination Place.

## Draw Route in the Map.

Now that we have the information for `currentPlace` and `destinationPlace`, we can **draw a route in the Map**.

We use [@mapbox/polyline](https://github.com/mapbox/polyline) library to draw the route combined with directions API. 

First, let's install mapbox/polyline in our app

```
npm install @mapbox/polyline
```

Second, let's create a new utility function inside `src/utils/index.js` 
```js
import PoliLyne from '@mapbox/polyline';
...
export const fetchRoute = async (originPlaceId, destinationPlaceId) => {
  try {
    const res = await fetch(
      `https://maps.googleapis.com/maps/api/directions/json?origin=place_id:${originPlaceId}&destination=place_id:${destinationPlaceId}&key=${GOOGLE_MAPS_API_KEY}`,
    );
    const json = await res.json();
    if (!json.routes[0]) {
      return;
    }
    const points = PoliLyne.decode(json.routes[0].overview_polyline.points);
    const coordinates = points.map((point) => ({
      latitude: point[0],
      longitude: point[1],
    }));

    return coordinates;
  } catch (error) {
    console.log(error);
  }
};
```

### Directions API

Don't forget to activate Directions API inside your **console.cloud.google.com** project as I show in the next image

![Directions API](https://i.imgur.com/yrZO8eY.png)

Last step, let's open our **UserScreen** component and use the `fetchRoute` function we just added and the **Polyline** component from react-native-maps.

```js
import React, {useEffect, useState, useRef} from 'react';
import {StatusBar, Platform, Image} from 'react-native';
import styled from 'styled-components/native';
👉 import MapView, {PROVIDER_GOOGLE, Polyline, Marker} from 'react-native-maps';
import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions';
import Geolocation from 'react-native-geolocation-service';
import {customStyleMap, MenuButtonLeft} from '../styles';
import FeatherIcon from 'react-native-vector-icons/Feather';
import DepartureInformation from '../components/DepartureInformation';
import Geocoder from 'react-native-geocoding';
import {usePlace} from '../context/PlacesManager';
import {GOOGLE_MAPS_API_KEY} from '../utils/constants';
import marker from '../assets/icons-marker.png';
import BookingInformation from '../components/BookingInformation';
import {useShowState} from '../hooks';
👉 import {fetchRoute} from '../utils';

...

const UserScreen = ({navigation}) => {
  const [location, setLocation] = useState(null);
  const {
    place: {currentPlace, destinationPlace}, 👈
    dispatchPlace,
  } = usePlace();
  const [showBooking, toggleShowBookingViews] = useShowState(false);
👉  const [polilyneCoordinates, setPolilyneCoordinates] = useState([]);
👉  const mapRef = useRef(null);

  const handleLocationPermission = async () => {
    ...
  };

  useEffect(() => {
    handleLocationPermission();
  }, []);

  useEffect(() => {
    ...
  }, [dispatchPlace]);

  const onRegionChange = ({latitude, longitude}) => {
    ...
  };

  useEffect(() => {
    ...
  }, [navigation]);

👇
  useEffect(() => {
    if (currentPlace.placeId && destinationPlace.placeId) {
      fetchRoute(currentPlace.placeId, destinationPlace.placeId).then(
        results => {
          setPolilyneCoordinates(results);
          mapRef.current.fitToCoordinates(results, {
            edgePadding: {left: 20, right: 20, top: 40, bottom: 60},
          });
        },
      );
    }
  }, [currentPlace, destinationPlace.placeId]);

  return (
    <Container>
      <StatusBar barStyle="dark-content" />
      {location && (
        <MapView
          testID="map"
        👉 ref={mapRef}
          style={mapContainer}
          provider={PROVIDER_GOOGLE}
          initialRegion={{
            latitude: location.latitude,
            longitude: location.longitude,
            latitudeDelta: 0.0922,
            longitudeDelta: 0.0421,
          }}
          onRegionChangeComplete={onRegionChange}
          showsUserLocation={true}
          customMapStyle={customStyleMap}
          paddingAdjustmentBehavior="automatic"
          showsMyLocationButton={true}
          showsBuildings={true}
          maxZoomLevel={17.5}
          loadingEnabled={true}
          loadingIndicatorColor="#fcb103"
          loadingBackgroundColor="#242f3e">
       👉  {polilyneCoordinates.length > 1 && (
       👉   <Polyline
              testID="route"
              coordinates={polilyneCoordinates}
              strokeWidth={3}
              strokeColor="#F4E22C"
            />
          )}

       👉  {polilyneCoordinates.length > 1 && (
            <Marker
              testID="destination-marker"
              coordinate={polilyneCoordinates[polilyneCoordinates.length - 1]}
            />
          )}
        </MapView>
      )}

     👉 {destinationPlace.placeId === '' && (
          <FixedMarker testID="fixed-marker">
             <Image style={markerStyle} source={marker} />
          </FixedMarker>
        )}

      {showBooking ? (
        <BookingInformation />
      ) : (
        <DepartureInformation toggleShowBookingViews={toggleShowBookingViews} />
      )}
    </Container>
  );
};

export default UserScreen;
```

A lot of things happened. First, we import **Polyline** component from react-native-maps to draw the PolyLines points from the `fetchRoute` function.

Second we added `{currentPlace, destinationPlace}` from `place` object.

Third, we added `polylineCoordinates` array using useState and created a local `mapRef` to access **MapView** components utility function.

```js
const [polilyneCoordinates, setPolilyneCoordinates] = useState([]);
const mapRef = useRef(null);
```
Fourth, we added a new useEffect that will call fetchRoute function if `currentPlace.placeId` and `destinationPlace.placeId` ins't null/false/undefined.

```js
useEffect(() => {
    if (currentPlace.placeId && destinationPlace.placeId) {
      fetchRoute(currentPlace.placeId, destinationPlace.placeId).then(
        results => {
          setPolilyneCoordinates(results);
          mapRef.current.fitToCoordinates(results, {
            edgePadding: {left: 20, right: 20, top: 40, bottom: 60},
          });
        },
      );
    }
  }, [currentPlace, destinationPlace.placeId]);
```

Once we have the array of PolyLines points, we update the `polylineCoordinates` local state and call `fitToCoordinates` function from MapView to update the padding of the **MapView** component.

Fifth, we need to pass the `mapRef` into MapView and check if we have PolyLines to draw the route. If we have the route we add a Marker for the last PolyLine point.

```js
{location && (
    <MapView
       ...
       ref={mapRef}
       ...
    >
     {polilyneCoordinates.length > 1 && (
            <Polyline
              testID="route"
              coordinates={polilyneCoordinates}
              strokeWidth={3}
              strokeColor="#F4E22C"
            />
          )}

          {polilyneCoordinates.length > 1 && (
            <Marker
              testID="destination-marker"
              coordinate={polilyneCoordinates[polilyneCoordinates.length - 1]}
            />
          )}
        </MapView>
      )}

      {destinationPlace.placeId === '' && (
        <FixedMarker testID="fixed-marker">
          <Image style={markerStyle} source={marker} />
        </FixedMarker>
      )}
```

Lastly, we add a condition to hide the Marker that we used as reference in the beginning.

![Draw a route](https://media.giphy.com/media/zoFFixtnRKHdvM1lr8/giphy.gif)

## Unit Tests

It's test time! 😍

This time will test Prediction component. Create a new test file inside `src/components/__tests__/Prediction.test.js`:

```js
import React from 'react';
import {render, fireEvent} from '@testing-library/react-native';
import Prediction from '../Prediction';
import {PlaceContext} from '../../context/PlacesManager';

describe('<Prediction />', () => {
  test('is tappable', async () => {
    const place = {description: 'Domkyrkan', placeId: '123'};
    const mockToggleModal = jest.fn();
    const dispatchPlace = jest.fn();
    const {getByText} = render(
      <PlaceContext.Provider value={{place, dispatchPlace}}>
        <Prediction
          description={place.description}
          place_id={place.placeId}
          toggleModal={mockToggleModal}
        />
      </PlaceContext.Provider>,
    );

    fireEvent.press(getByText('Domkyrkan'));
    expect(dispatchPlace).toHaveBeenCalled();
    expect(mockToggleModal).toHaveBeenCalled();
  });
});
```