DEV Community

loading...
Cover image for React Native Taxi App. Google Maps Region Change. React Navigation.

React Native Taxi App. Google Maps Region Change. React Navigation.

Cristian Echeverria
I try to be a good human who cares about family & friends. I Speak both Spanish and English.
・9 min read

Source Code Part 2 and 3

Part 3. Google Maps Region Change. React Navigation & Unit Tests.

Let’s add new functionality where we can drag and move the Map around and get the Location Place while we move around. We will use an image similar to a Marker as a reference point.

Open <UserScreen /> component and we will add a new MapView prop called onRegionChangeComplete.

onRegionChangeComplete

...
 // Add this function to update Location Place
 const onRegionChange = ({latitude, longitude}) => {
     // using Geocoder we will fetch new location information
    Geocoder.from({
      latitude,
      longitude,
    }).then(res => {
      const {
        formatted_address,
        place_id,
        geometry: {
          location: {lat, lng},
        },
      } = res.results[0];
    // Once we have a response we dispatch & update currentPlace
      dispatchPlace({
        type: 'SET_CURRENT_PLACE',
        description: formatted_address,
        placeId: place_id,
        latitude: lat,
        longitude: lng,
      });
    });
  };

  return (
    <Container>
      <StatusBar barStyle="dark-content" />
      {location && (
        <MapView
          ...
          onRegionChangeComplete={onRegionChange} // 👈
          ...
        />
      )}
      <DepartureInformation />
    </Container>
  );
};
Enter fullscreen mode Exit fullscreen mode

OnRegionChange

Now, let’s add an image similar to a Marker. First, we have to select the image. For this project, I decided to use this image that you can download too. Right-click on the image below and save it inside your project folder at src/assets/:

Icon Marker

After saving the image in our new assets folder, we will import the new asset inside the <UserScreen /> component. Also, we will create a new Styled component where will render the image.

...
// Add Image component from react-native
import {StatusBar, Platform, Image} from 'react-native';
// Import icon image
import marker from '../assets/icons-marker.png';
...

// Create Styled component.
// This component its almost right in the middle of our Emulator.
// We have to play a bit with the margin-top property.
const FixedMarker = styled.View`
  left: 50%;
  margin-left: -16px;
  margin-top: -125px;
  position: absolute;
  top: 50%;
`;
// This will be the marker Size
const markerStyle = {
  height: 36,
  width: 36,
};

return (
    <Container>
      <StatusBar barStyle="dark-content" />
      {location && (
        <MapView
          ...
          onRegionChangeComplete={onRegionChange}
          ...
        />
      )}

      <FixedMarker testID="fixed-marker">
        <Image style={markerStyle} source={marker} />
      </FixedMarker>

      <DepartureInformation />
    </Container>
  );
Enter fullscreen mode Exit fullscreen mode

Icon Marker on Map

Alright!, as you can see, we have an image as a reference point. I know, I know, you don’t have to yell at me because the image isn’t perfectly aligned. Trust me; I’m not blind. We will try to fix that a bit later. For now, leave it as it’s.

React Navigation

Although we only have one screen (<UserScreen />), I need to add the Header navigation bar into our component to fix the Image Marker. So let’s add React Navigation, and later we will add more screens to navigate to and from.

npm install @react-navigation/native --save-exact
Enter fullscreen mode Exit fullscreen mode

We're not using expo, so we will install the librarires required for RN CLI.

npm install --save-exact react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
Enter fullscreen mode Exit fullscreen mode

If you're on a Mac and developing for iOS, you need to install the pods (via Cocoapods) to complete the linking.

npx pod-install ios
Enter fullscreen mode Exit fullscreen mode

To finalize installation of react-native-gesture-handler, add the following at the top (make sure it's at the top and there's nothing else before it) of your entry file, such as index.js:

import "react-native-gesture-handler" // 👈
import { AppRegistry } from "react-native"
import App from "./src/App"
import { name as appName } from "./app.json"

AppRegistry.registerComponent(appName, () => App)
Enter fullscreen mode Exit fullscreen mode

Stack Navigator

We're going to use Stack Navigation. That's why we need to install it. Follow this link to find out more:

npm install @react-navigation/stack --save-eact
Enter fullscreen mode Exit fullscreen mode

Let's import createStackNavigator and Navigation Container into our src/App.js file:

import React from "react"
// NavigationContainer
import { NavigationContainer } from "@react-navigation/native"
// createStackNavigator
import { createStackNavigator } from "@react-navigation/stack"
import UserScreen from "./screens/UserScreen"
import { PlaceProvider } from "./context/PlacesManager"

// Create the Stack
const Stack = createStackNavigator()

const App = () => {
  return (
    <PlaceProvider>
      <NavigationContainer>
        <Stack.Navigator mode="modal">
          <Stack.Screen
            name="User"
            component={UserScreen}
            options={() => ({
              headerTitle: "Taxi App",
            })}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </PlaceProvider>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

React Navigation Header

After that, we now see our Image Marker with a better alignment because of the Header from React Navigation. With the Image Marker, we can use it as a reference to move around the Map and get the place name.

App Menu

It's time for add the Menu Icon and the MenuScreen to our app. Inside src/screens/ create a new component called MenuScreenModal.js.

// ./src/screens/MenuScreenModal.js
import React from "react"
import { FlatList, TouchableWithoutFeedback } from "react-native"
// We have to create new global styles
import { SignInButtonText, SignInButton } from "../styles"
import FeatherIcon from "react-native-vector-icons/Feather"
import styled from "styled-components/native"

// This is the Menu of our app
const MENU = [
  {
    id: "1",
    title: "Bookings",
    icon: "map-pin",
  },
  {
    id: "2",
    title: "Receipts",
    icon: "file-text",
  },
  {
    id: "3",
    title: "Profile",
    icon: "user",
  },
  {
    id: "4",
    title: "Cards",
    icon: "credit-card",
  },
]

const Container = styled.View`
  flex: 1;
  padding-vertical: 100px;
  padding-left: 10px;
  background-color: #fff;
  padding-horizontal: 20px;
`

const MenuItemContainer = styled.View`
  padding-vertical: 10px;
`

const MenuItemView = styled.View`
  flex-direction: row;
  align-items: baseline;
`

const MenuItemText = styled.Text`
  font-size: 26px;
  font-weight: bold;
  margin-left: 10px;
`

const SignInContainer = styled.View`
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
`

// Here we define the styling of each menu item.
const MenuItem = ({ title, icon, navigation }) => (
  <MenuItemContainer>
    <TouchableWithoutFeedback
      onPress={() => navigation.navigate(title)}
      testID={`menuItem-${title}`} // 👈 testID for testing purposes.
    >
      <MenuItemView>
        <FeatherIcon name={icon} size={25} color="#000" />
        <MenuItemText>{title}</MenuItemText>
      </MenuItemView>
    </TouchableWithoutFeedback>
  </MenuItemContainer>
)

export default function MenuScreenModal({ navigation }) {
  const renderMenuItem = ({ item }) => (
    <MenuItem {...item} navigation={navigation} />
  )

  // Using FlatList component from react-native we show list of Menu
  // Also a 'Sign In / Sign Up' button

  return (
    <Container>
      <FlatList
        data={MENU}
        renderItem={renderMenuItem}
        keyExtractor={item => item.id}
      />
      <SignInContainer>
        <SignInButton
          onPress={() => console.log("Sign In / Sign Up Pressed")}
          testID="signInCheck-button/" // 👈 testID for testing purposes.
        >
          <SignInButtonText>Sign In / Sign Up</SignInButtonText>
        </SignInButton>
      </SignInContainer>
    </Container>
  )
}
Enter fullscreen mode Exit fullscreen mode

To continue, let’s create the two new global styles we’re importing for the MenuScreenModal component—open src/styles/index.js.

export const SignInButtonText = styled.Text`
  font-weight: bold;
  font-size: 15px;
`

export const SignInButton = styled.TouchableOpacity`
  align-items: center;
  background-color: #f4e22c;
  padding: 10px;
  border-radius: 20px;
  width: 100%;
  margin-left: auto;
`
Enter fullscreen mode Exit fullscreen mode

After that, let's create a new Stack.Screen component for our MenuScreenModal inside src/App.js.

...
// Import MenuScreenModal component
import MenuScreenModal from './screens/MenuScreenModal';
// Import a new global style
import {MenuButtonLeft} from './styles';
import FeatherIcon from 'react-native-vector-icons/Feather';

const Stack = createStackNavigator();

const App = () => {
  return (
    <PlaceProvider>
      <NavigationContainer>
        <Stack.Navigator mode="modal">
          ...
          <Stack.Screen
              name="Menu"
              component={MenuScreenModal}
              options={({navigation}) => ({
                headerLeft: () => (
                  <MenuButtonLeft
                    onPress={() => navigation.goBack()}
                    testID="back-menu">
                    <FeatherIcon
                      name="x"
                      size={25}
                      color="#000"
                      testID="close-menu"
                    />
                  </MenuButtonLeft>
                ),
                headerTitle: '',
              })}
            />
        </Stack.Navigator>
      </NavigationContainer>
    </PlaceProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

We added a new Stack.Screen component that render the <MenuScreenModal /> component. Notice how we added a couple of options into the Screen, like headerLeft & headerTitle.

headerLeft will render a back menu with a close-menu icon from FeatherIcons to close the Menu on press.

headerTitle will override the title for the Screen. If you don’t define it will take the screen name as the title by default.

If you take a closer look at the <MenuButtonLeft /> styled component, you will see that it has an onPress prop thal call navigation.goBack() function, and that’s because when we use React Navigation and the <NavigationContainer>, we have access to the navigation object prop in all the screens we define.

Lastly, let’s create a button inside the <UserScreen /> component that will open our MenuScreenModal.

// ./src/screens/UserScreen.js
...
// Import MenuButtonLeft style
import {customStyleMap, MenuButtonLeft} from '../styles';
import FeatherIcon from 'react-native-vector-icons/Feather';
...
// Insert the 'navigation' prop from <NavigationContainer>
// See how its wrapper in {}, tha's because we are destructuring the props object.
// Otherwise would be just 'props' and then 'props.navigation.setOptions' inside useEffect.
export default function UserScreen({navigation}) {
  ...

 // We use useEffect that means when Component Did Mount
 // Pass the 'nagivation' prop because its used to call 'setOptions' function
  useEffect(() => {
    navigation.setOptions({
      headerLeft: () => (
          <MenuButtonLeft
            onPress={() => navigation.navigate('Menu')}
            testID="modal-menu">
            <FeatherIcon name="menu" size={25} color="#000" />
          </MenuButtonLeft>
        ),
    });
  }, [ navigation ]);

  ...
Enter fullscreen mode Exit fullscreen mode

MenuScreen Modal

So, we can define Stack.Screen options when we declare the screen inside App.js, or modify the options inside every screen component using navigation.setOptions prop, which is great because we can update those options dynamically. I will do that later.

Unit Tests

It's Tests time! 😍

First thing, first, let’s rerun the tests suite and see what fails after our recent updates.

npm run test
Enter fullscreen mode Exit fullscreen mode

Tests Fail

Well, well, it seems that <DepartureInformation /> tests pass and <UserScreen /> fail.

 console.error
      Warning: An update to UserScreen inside a test was not wrapped in act(...).

      When testing, code that causes React state updates should be wrapped into act(...):

      act(() => {
        /* fire events that update state */
      });
      /* assert on the output */

      This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
          at UserScreen

       97 |             },
       98 |           } = res.results[0];
    >  99 |           setLocation({latitude, longitude});
          |           ^
      100 |           dispatchPlace({
Enter fullscreen mode Exit fullscreen mode

We got the well-known wrapped into act() warning, and that’s because we update the local state without waiting for the component to fulfill that promise. You can find a convenient and more detailed guide about that in one of Kent C. Dodds posts here.

The second reason that fails is that setOptions from navigation object that it’s undefined; see here:

 <UserScreen />  should renders MapView and Marker with user current location

    TypeError: Cannot read property 'setOptions' of undefined

      138 |
      139 |   useEffect(() => {
    > 140 |     navigation.setOptions({
          |                ^
      141 |       headerLeft: () => (
      142 |         <MenuButtonLeft
      143 |           onPress={() => navigation.navigate('Menu')}
Enter fullscreen mode Exit fullscreen mode

Let's fix those issues! 💪

Open src/screens/__tests__/UserScreen.test.js:

import React from "react"
// Import act from testing-library
import { render, waitFor, act } from "@testing-library/react-native"
...

describe("<UserScreen />", () => {
  const place = {
    currentPlace: {
      description: "Keillers Park",
      placeId: "abc",
      latitude: 57.7,
      longitude: 11.93,
    },
  }
  const dispatchPlace = jest.fn()
  // Mock navigation prop
  // Also declare the navigation prop when component render bellow
  // See how we mocked setOptions as a jest function.
  const navigation = {
    setOptions: jest.fn(),
  }

  test("should renders MapView and Marker with user current location", async () => {
    const { getByTestId } = render(
      <PlaceContext.Provider value={{ place, dispatchPlace }}>
        <UserScreen navigation={navigation} />
      </PlaceContext.Provider>
    )

    await waitFor(() => {
      expect(check).toHaveBeenCalledTimes(1)
      expect(Geolocation.getCurrentPosition).toHaveBeenCalledTimes(1)
      expect(Geocoder.from).toHaveBeenCalledWith({
        latitude: 57.7,
        longitude: 11.93,
      })
      expect(getByTestId("map")).toBeDefined()
    })
  })

 // Added a new test case for Context Providers
  test("should have called Context Providers", async () => {
    render(
      <PlaceContext.Provider value={{ place, dispatchPlace }}>
        <UserScreen navigation={navigation} />
      </PlaceContext.Provider>
    )

    // Here we await the fulfillment of setLocation({...})
    // This updates our local state
    await act(() => Promise.resolve())

    // Then we can add assertions. Once the promise was fulfill.
    // See how we test the disptachPlace action
    expect(dispatchPlace).toHaveBeenCalledWith({
      type: "SET_CURRENT_PLACE",
      description: "Lindholmen",
      placeId: "abc",
      latitude: 57.7,
      longitude: 11.93,
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Would you please read the comments above to understand what happened? If we rerun the test, should it be now all green?. Yes!

Now, let's add a new tests file for <MenuScreenModal /> component that we just created. Create a new file inside src/screens/__tests__/MenuScreenModal.test.js:

// src/screens/__tests__/MenuScreenModal.test.js
import React from "react"
import { render } from "@testing-library/react-native"
import MenuScreenModal from "../MenuScreenModal"

describe("<MenuScreenModal />", () => {
  // Mocking navigation object this time the 'navigate' function
  // Navigate function is responsible for pushing us to the next screen
  const navigation = {
    navigate: jest.fn(),
  }
  test("should render list of menu and Sign In/Sign Up button", () => {
    // navigation is a prop we defined
    const { getByTestId } = render(<MenuScreenModal navigation={navigation} />)

    // Here we use the testID we defined inside <MenuScreenModal />
    expect(getByTestId(/menuItem-Bookings/)).toBeDefined()
    expect(getByTestId(/menuItem-Receipts/)).toBeDefined()
    expect(getByTestId(/menuItem-Profile/)).toBeDefined()
    expect(getByTestId(/menuItem-Cards/)).toBeDefined()
    expect(getByTestId(/signInCheck-button/)).toBeDefined()
  })
})
Enter fullscreen mode Exit fullscreen mode

Tests Pass

Just a comment regarding Unit Testing, when I test, I’m testing the user behavior in our app. For this case and all the previous tests cases, I’m testing what the user should see on the screen, and also we test what happens when component mount and when the user triggers an action like press a button, etc.

I don’t care about the code implementation when I’m testing. I just care about the expected behavior. Maybe not all the behaviors, but at least the most important ones.

🛑 Stop!

That's it from me for now. I hope you're doing good by now and learning a thing or two. I Will add Part 2 & Part 3 source code here. I forgot to commit Part 2 updates to the GitHub repo so that it will be together 🙈.

Discussion (0)