How to Add a Vector Map to a React Native App with MapLibre

API or service
Map Tiles
Task
Expo app + Geoapify style URL → native vector map
Examples
React NativeAI
Difficulty
Intermediate
Time
20 min

You are building a React Native app and need an interactive vector map: smooth zoom, sharp labels on high-density screens, and switchable light and dark designs. MapLibre React Native renders vector maps natively on iOS and Android, and Geoapify Map Tiles provide complete map styles as style.json URLs.

The user input is a style name and an initial view (center and zoom). The result is a full-screen native map that loads its tiles, fonts, icons, and attribution from the Geoapify style.

Task flow: Expo development build with MapLibre React Native → Map with a Geoapify style URL → Camera with the initial view → markers and GeoJSON layers on top.

The examples use Expo SDK 57, React Native 0.86 (new architecture), MapLibre React Native 11.5, and TypeScript, centered on Santa Fe Plaza, New Mexico. MapLibre React Native contains native code, so it does not run in Expo Go; the app runs as a development build.

Set up an Expo development build

Add MapLibre React Native and the Expo development client to an Expo project. @types/geojson provides the GeoJSON types used for map data:

npx expo install @maplibre/maplibre-react-native expo-dev-client
npm install --save-dev @types/geojson

npx expo install also registers the library's config plugin. Check that app.json lists it in plugins; on iOS, the plugin adds MapLibre's step to the generated Podfile:

{
  "expo": {
    "plugins": [
      "@maplibre/maplibre-react-native"
    ]
  }
}

iOS 27: iOS 27 requires the UIScene life cycle, and an Expo SDK 57 app without it stops at launch with a SIGTRAP crash. Turn on Expo's scene support with expo-build-properties (Expo 57.0.25 or newer) until a later SDK enables it by default:

npx expo install expo-build-properties
{
  "expo": {
    "plugins": [
      "@maplibre/maplibre-react-native",
      ["expo-build-properties", { "ios": { "enableSceneSupport": true } }]
    ]
  }
}

Store the Geoapify API key in a .env file next to package.json, and add .env to .gitignore. Expo inlines variables with the EXPO_PUBLIC_ prefix into the JavaScript bundle:

EXPO_PUBLIC_GEOAPIFY_API_KEY=YOUR_API_KEY

Build and start the app on a simulator, emulator, or device:

npx expo run:ios
# or
npx expo run:android

expo run generates the native projects with the config plugins and installs the development build. iOS builds need macOS with Xcode and CocoaPods; Android builds need Android Studio and the Android SDK. The first build downloads and compiles MapLibre Native and takes several minutes.

Show a Geoapify vector map in React Native

Pass a Geoapify style.json URL to the mapStyle prop of Map, and set the initial view with Camera. The style URL has the form https://maps.geoapify.com/v1/styles/<style>/style.json?apiKey=YOUR_API_KEY:

import { StyleSheet, View } from "react-native";
import { Camera, Map, type LngLat } from "@maplibre/maplibre-react-native";

// Read from .env: EXPO_PUBLIC_GEOAPIFY_API_KEY=...
const API_KEY = process.env.EXPO_PUBLIC_GEOAPIFY_API_KEY;

const styleUrl = (style: string) =>
  `https://maps.geoapify.com/v1/styles/${style}/style.json?apiKey=${API_KEY}`;

// Santa Fe Plaza, New Mexico, in [longitude, latitude] order
const PLAZA: LngLat = [-105.9384, 35.6872];

export default function App() {
  return (
    <View style={styles.container}>
      <Map style={styles.map} mapStyle={styleUrl("osm-bright")}>
        <Camera initialViewState={{ center: PLAZA, zoom: 15 }} />
      </Map>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  map: { flex: 1 },
});

The Map component loads the style, then the vector tiles, fonts, and icons that the style lists. Coordinates are [longitude, latitude]. Importing Map from the library shadows the built-in JavaScript Map in this file.

The map attribution comes from the style, and MapLibre React Native shows it through its attribution button. Keep it enabled; the last step lists the required credits.

To switch to another Geoapify style, pass a different URL. With React state, a light/dark toggle is a single prop change:

const [dark, setDark] = useState(false);

<Map style={styles.map} mapStyle={styleUrl(dark ? "dark-matter" : "osm-bright")}>
  <Camera initialViewState={{ center: PLAZA, zoom: 15 }} />
</Map>

Markers and layers declared as children of Map stay on top of the new style. Each style has its own tiles, so the first switch to a style loads new tiles. See the Map Tiles documentation for the available style names.

Add a marker and a GeoJSON layer

Add map content as children of Map. A Marker renders a React Native view at a coordinate; use it for a few interactive or custom-styled points:

import { Marker } from "@maplibre/maplibre-react-native";

<Marker id="plaza" lngLat={PLAZA} anchor="bottom">
  <View style={styles.pin}>
    <Text style={styles.pinText}>Plaza</Text>
  </View>
</Marker>

anchor="bottom" places the bottom edge of the view on the coordinate, so a pin-shaped view points at the location.

For many points, draw them with a GeoJSONSource and a style Layer. MapLibre renders the layer natively, which scales to hundreds of features. This example draws cafés returned by the Places API, whose response is already a GeoJSON FeatureCollection:

import { GeoJSONSource, Layer } from "@maplibre/maplibre-react-native";

{cafes && (
  <GeoJSONSource id="cafes" data={cafes}>
    <Layer
      id="cafe-circles"
      type="circle"
      paint={{
        "circle-radius": 7,
        "circle-color": "#e8762d",
        "circle-stroke-width": 2,
        "circle-stroke-color": "#ffffff",
      }}
    />
  </GeoJSONSource>
)}

cafes is the FeatureCollection from this request, which returns up to 20 cafés within 800 metres of the plaza, ordered by distance:

https://api.geoapify.com/v2/places?categories=catering.cafe&filter=circle:-105.9384,35.6872,800&bias=proximity:-105.9384,35.6872&limit=20&apiKey=YOUR_API_KEY

Load it with fetch in a useEffect, check response.ok, and abort the request when the component unmounts. Render the source only after the data has arrived, as above. The paint properties use the MapLibre style specification.

Ship the map: attribution, cost, and API key

Attribution. A map created from a Geoapify style.json carries its attributions, and MapLibre React Native shows them through the attribution button. Keep it enabled (do not set attribution={false}). If you hide or replace it, show the required credits yourself, as described in Map Tiles attribution:

  • Powered by Geoapify: required on the Free plan;
  • © OpenStreetMap contributors: the map data is licensed under ODbL 1.0;
  • © OpenMapTiles: the vector tiles use the OpenMapTiles schema.

Cost. Each map tile request costs 0.25 credits, so the Free plan's 3,000 daily credits cover about 12,000 tile requests per day. Tile usage depends on screen size, zoom changes, and how far users pan; run the app on the Free plan and watch the statistics of your Geoapify project to estimate it. See Map Tiles pricing and the Pricing page.

API key. An EXPO_PUBLIC_ variable is compiled into the app bundle, so anyone who unpacks the app can read the key. Treat it as public:

  • Create a separate key for the mobile app, so you can monitor and replace it independently.
  • Watch its usage in your Geoapify project.
  • Geoapify key restrictions work by IP address, HTTP referrer, origin, and CORS, which native apps do not send, so they do not protect a key inside a mobile app.
  • For stricter control of other APIs, such as Places, call them through your own backend and keep that key on the server.

Before release, build and run the app on both platforms. MapLibre React Native 11 supports only the React Native new architecture.

React Native map screen with an AI coding agent

Copy this prompt into Codex, Claude Code, Cursor, or another coding agent to add a Geoapify vector map screen to your React Native app.

Add a map screen with a Geoapify vector map to my React Native app using MapLibre React Native.

Before you start
- Check whether the project uses Expo or bare React Native, which SDK and React Native versions, and whether the new architecture is enabled. MapLibre React Native 11 requires the new architecture and does not run in Expo Go; with Expo, use a development build (expo-dev-client) and the @maplibre/maplibre-react-native config plugin.
- Ask for the initial center and zoom, the map styles to offer (for example osm-bright and dark-matter), and what to draw on the map (markers, GeoJSON layers).
- Ask me to configure a Geoapify API key. With Expo, read it from EXPO_PUBLIC_GEOAPIFY_API_KEY in .env and add .env to .gitignore. Never hardcode, print, or log the key.

Implementation
1. Install @maplibre/maplibre-react-native (and expo-dev-client with Expo) and confirm the config plugin is registered. For Expo SDK 57 apps targeting iOS 27, enable scene support with expo-build-properties: "ios": { "enableSceneSupport": true }.
2. Render <Map mapStyle={`https://maps.geoapify.com/v1/styles/${style}/style.json?apiKey=${key}`}> with a <Camera initialViewState={{ center: [lon, lat], zoom }} />.
3. Add markers as <Marker lngLat={[lon, lat]} anchor="bottom"> and many points as <GeoJSONSource> with a <Layer>, as children of Map.
4. If a style switch is needed, change the mapStyle URL from state.

Requirements
- Use [longitude, latitude] order everywhere.
- Keep the attribution button enabled; it shows the credits that the Geoapify style provides.
- Handle a missing API key with a clear message instead of a blank map.
- Explain that a key inside a mobile bundle is public, and suggest a separate key for the app.
- Give the commands to build and run on iOS and Android, and note that the first native build takes several minutes.

Review the generated code against this guide. Check the style URL, coordinate order, the config plugin, the new-architecture and development-build requirements, attribution, and API-key handling.