loading...
Cover image for An Introduction to React Native Maps in Expo with TypeScript

An Introduction to React Native Maps in Expo with TypeScript

peterklingelhofer profile image Peter Klingelhofer ・5 min read

Introduction

Expo is a fast an easy way to get started creating mobile apps with React Native, and all without needing MacOS to test on your iPhone. Used in conjunction with react-native-maps, you can have an interactive Map up and running on a phone in no time.

Getting Started with Expo

First, install Expo globally with npm i expo -g. Then, use expo init to create an Expo project! Name your project, and then select "tabs (TypeScript)" under Managed workflow. Name your app.

Alt Text

Then run expo init, install the Expo app on your mobile device, open camera, and scan the QR code you see in the terminal or web browser that opens up. You should see the template load up with two clickable tabs at the bottom.

Alt Text

Your First Map

Now run expo install react-native-maps. This will install the latest react-native-maps along with any dependencies for the current SDK (38.0 at the time of writing this blog post).

Now replace TabOnScreen.tsx with the following:

// TabOnScreen.tsx
import React, { useState } from 'react';
import { StyleSheet, Dimensions } from 'react-native';

import { View } from '../components/Themed';
import MapView from 'react-native-maps';

const { width, height } = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE = 29.9990674;
const LONGITUDE = -90.0852767;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

export default function TabOneScreen({ provider }) {
  const [region, setRegion] = useState({
    latitude: LATITUDE,    // initial location latitude
    longitude: LONGITUDE,  // initial location longitude
    latitudeDelta: LATITUDE_DELTA,
    longitudeDelta: LONGITUDE_DELTA,
  });
  return (
    <View style={styles.container}>
      <MapView
        provider={provider}
        style={styles.map}
        initialRegion={region}
        zoomTapEnabled={false}
      ></MapView>
    </View>
  );
}

const styles = StyleSheet.create({
  map: {
    ...StyleSheet.absoluteFillObject,
  },
});

Maps on iOS
Here we created a simple MapView component from react-native-maps.

Maps with Markers and Custom Callouts

Now for an example with multiple different types of InfoWindows that pop up on Markers with information referring to that specific marker. In react-native-maps, these are referred to as Callouts.

Map with Markers
This will demonstrate three different types of Callouts that we can use with our Markers. Here, we have buttons to show and hide the Callout for the last selected Marker, and an overlay with just some explanatory text as well.

First, create new file called CustomCallouts.ts and place the code from here into it.

Then, in TabTwoScreen.tsx, place the following code below:

// TabTwoScreen.tsx
import React, { useState } from 'react';
import { StyleSheet, Dimensions, TouchableOpacity, Alert } from 'react-native';

import { Text, View } from '../components/Themed';
import MapView, { Marker, Callout, CalloutSubview } from 'react-native-maps';
import CustomCallout from './CustomCallout';

const { width, height } = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE = 29.9990674;
const LONGITUDE = -90.0852767;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;
const SPACE = 0.01;

export default function TabTwoScreen({provider}) {
  const [count, setCount] = useState(0);
  const [region, setRegion] = useState({
    latitude: LATITUDE,
    longitude: LONGITUDE,
    latitudeDelta: LATITUDE_DELTA,
    longitudeDelta: LONGITUDE_DELTA,
  });
  const [markers, setMarkers] = useState([
    {
      coordinate: {
        latitude: LATITUDE + SPACE,
        longitude: LONGITUDE + SPACE,
      },
    },
    {
      coordinate: {
        latitude: LATITUDE + SPACE,
        longitude: LONGITUDE - SPACE,
      },
    },
    {
      coordinate: {
        latitude: LATITUDE,
        longitude: LONGITUDE,
      },
    },
    {
      coordinate: {
        latitude: LATITUDE,
        longitude: LONGITUDE - SPACE / 2,
      },
    },
  ]);

  const [markerRefs, setMarkerRefs] = useState([
    {
      ref: null,
    },
    {
      ref: null,
    },
    {
      ref: null,
    },
    {
      ref: null,
    },
  ]);

  const show = () => {
    markerRefs[0].ref.showCallout();
  };

  const hide = () => {
    markerRefs[0].ref.showCallout();
  };

  return (
    <View style={styles.container}>
      <MapView
        provider={provider}
        style={styles.map}
        initialRegion={region}
        zoomTapEnabled={false}
      >
        <Marker
          ref={(ref) => {
            let updateRef = markerRefs;
            updateRef[0].ref = ref;
            setMarkerRefs(updateRef);
          }}
          coordinate={markers[0].coordinate}
          title="This is a native view"
          description="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
        />
        <Marker coordinate={markers[1].coordinate}>
          <Callout style={styles.plainView}>
            <View>
              <Text>This is a plain view</Text>
            </View>
          </Callout>
        </Marker>
        <Marker
          coordinate={markers[2].coordinate}
          calloutOffset={{ x: -8, y: 28 }}
          calloutAnchor={{ x: 0.5, y: 0.4 }}
          ref={(ref) => {
            let updateRef = markerRefs;
            updateRef[1].ref = ref;
            setMarkerRefs(updateRef);
          }}
        >
          <Callout
            alphaHitTest
            tooltip
            onPress={(e) => {
              if (
                e.nativeEvent.action === 'marker-inside-overlay-press' ||
                e.nativeEvent.action === 'callout-inside-press'
              ) {
                return;
              }

              Alert.alert('callout pressed');
            }}
            style={styles.customView}
          >
            <CustomCallout>
              <Text>{`This is a custom callout bubble view ${count}`}</Text>
              <CalloutSubview
                onPress={() => {
                  setCount(count + 1);
                }}
                style={[styles.calloutButton]}
              >
                <Text>Click me</Text>
              </CalloutSubview>
            </CustomCallout>
          </Callout>
        </Marker>
        <Marker
          ref={(ref) => {
            let updateRef = markerRefs;
            updateRef[3].ref = ref;
            setMarkerRefs(updateRef);
          }}
          coordinate={markers[3].coordinate}
          title="You can also open this callout"
          description="by pressing on transparent area of custom callout"
        />
      </MapView>
      <View style={styles.buttonContainer}>
        <View style={styles.bubble}>
          <Text>Tap on markers to see different callouts</Text>
        </View>
      </View>
      <View style={styles.buttonContainer}>
        <TouchableOpacity
          onPress={() => show()}
          style={[styles.bubble, styles.button]}
        >
          <Text>Show</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => hide()}
          style={[styles.bubble, styles.button]}
        >
          <Text>Hide</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  customView: {
    width: 140,
    height: 140,
  },
  plainView: {
    width: 60,
  },
  container: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'flex-end',
    alignItems: 'center',
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
  bubble: {
    flex: 1,
    backgroundColor: 'rgba(255,255,255,0.7)',
    paddingHorizontal: 18,
    paddingVertical: 12,
    borderRadius: 20,
  },
  latlng: {
    width: 200,
    alignItems: 'stretch',
  },
  button: {
    width: 80,
    paddingHorizontal: 12,
    alignItems: 'center',
    marginHorizontal: 10,
  },
  buttonContainer: {
    flexDirection: 'row',
    marginVertical: 20,
    backgroundColor: 'transparent',
  },
  calloutButton: {
    width: 'auto',
    backgroundColor: 'rgba(255,255,255,0.7)',
    paddingHorizontal: 6,
    paddingVertical: 6,
    borderRadius: 12,
    alignItems: 'center',
    marginHorizontal: 10,
    marginVertical: 10,
  },
});

One Marker selected with a Title and Description
The first kind of Callout just has a simple Title and Description.

Another Marker selected with different formatting and just a description
The second Callout uses the plain format, and only has a description, and has a darker pop-up window.

Marker with clickable button that adds 1 to counter, current counter value 0
Third, we have a CustomCallout with a clickable button, and a default value of 0. Let's observe what happens on button-click.

Marker with pressable button that adds 1 to counter, current counter value 1
Ah, the magic of useState! On press, the counter increments up by 1, and the value is now 1.

Marker with clickable button that adds 1 to counter, current counter value 2
And the same thing happens when we press the button again, the total is now 2.

Conclusion

For further exploration with Markers in react-native-maps, I recommend adding an image tag to your Marker, as is done with this example. react-native-maps is super responsive in Expo, and adding maps to your app is an easy way to impress. For further reading, check out the react-native-maps documentation. Additionally, for further inspiration, look through the folder of examples on GitHub.

Posted on by:

Discussion

pic
Editor guide