Surviving Custom Markers in React Native Maps

Well, I have to admit – I spent four hours last Tuesday staring at a blank beige grid on my iOS simulator. Just a sad, empty grid where a map should be. If you’ve built mobile apps long enough, you probably know exactly what I’m talking about. The dreaded initialization failure of react-native-maps.

You’d think dropping a map into an app would be a completely solved problem by now. It mostly is, assuming you just want a generic view of a city and absolutely zero custom UI. But the minute you need custom pins, clustering, or specific map styles, the abstraction leaks. Hard.

I’m currently running React Native 0.78.0 and react-native-maps 1.18.2 on my M3 Max MacBook Pro. The library has gotten better with the recent Fabric architecture updates, but it still hides some incredibly painful edge cases that the official documentation glosses over.

The Custom Marker Performance Trap

Here’s the scenario that burns almost everyone. You get the map working. You pull an array of locations from your backend. You map over them and render a custom <Marker> component with your own SVG icon or an image.

smartphone map pins - Smartphone world globe travel map pins | Premium Vector
smartphone map pins – Smartphone world globe travel map pins | Premium Vector

Suddenly, your app feels like it’s running through molasses.

The issue stems from how the native side handles view rendering. By default, react-native-maps tries to track changes to your custom marker views so it can redraw them if the React state changes. It does this constantly. And if you have 100 pins on the screen, the native bridge is doing an absurd amount of unnecessary work.

I hit this exact wall on a client project a few weeks ago. We had about 400 custom pins rendering. And every time the user panned the camera, the main thread would lock up for roughly 400ms. It was completely unusable.

But the fix? You have to manually strangle the tracksViewChanges prop.

import React, { useState, useEffect } from 'react';
import MapView, { Marker } from 'react-native-maps';
import { View, Image, StyleSheet } from 'react-native';

const OptimizedMarker = ({ coordinate, title }) => {
  // Force the marker to stop tracking view changes after initial render
  const [trackChanges, setTrackChanges] = useState(true);

  useEffect(() => {
    // Give the native side just enough time to draw the image once
    const timer = setTimeout(() => {
      setTrackChanges(false);
    }, 150);
    
    return () => clearTimeout(timer);
  }, []);

  return (
    <Marker 
      coordinate={coordinate}
      title={title}
      tracksViewChanges={trackChanges}
    >
      <View style={styles.pinContainer}>
        <Image 
          source={require('./assets/custom-pin.png')} 
          style={styles.pinImage} 
          // Crucial: Use onLoad to trigger the state change if you want to be perfectly precise
        />
      </View>
    </Marker>
  );
};

export default function AppMap({ locations }) {
  return (
    <MapView
      style={StyleSheet.absoluteFillObject}
      initialRegion={{
        latitude: 37.78825,
        longitude: -122.4324,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      }}
    >
      {locations.map(loc => (
        <OptimizedMarker 
          key={loc.id} 
          coordinate={loc.coords} 
          title={loc.name} 
        />
      ))}
    </MapView>
  );
}

const styles = StyleSheet.create({
  pinContainer: {
    width: 40,
    height: 40,
    justifyContent: 'center',
    alignItems: 'center',
  },
  pinImage: {
    width: 30,
    height: 30,
    resizeMode: 'contain',
  }
});

Implementing this stupidly simple state toggle dropped our memory usage by 38% and entirely eliminated the frame drops. And we went straight back to a solid 60fps during heavy panning.

The Google Maps vs Apple Maps Divide

smartphone map pins - Pin Multiple Locations on Map with MyRouteOnline Software
smartphone map pins – Pin Multiple Locations on Map with MyRouteOnline Software

Another thing that drives me crazy is the provider split. On Android, you’re using Google Maps. But on iOS, it defaults to Apple Maps (MapKit) unless you explicitly configure Google Maps.

And don’t try to force Google Maps on iOS unless your client absolutely demands visual parity across both platforms. I used to fight this battle constantly. I’d spend hours configuring CocoaPods and dealing with API keys just to make the iOS version look identical to the Android version.

But I stopped doing that. MapKit is significantly lighter on battery life for iOS users. Plus, forcing Google Maps into an iOS build inflates your app bundle size unnecessarily. Just let the platforms use their native defaults.

Though there is one massive gotcha with MapKit, though. If you pass an invalid coordinate (like a string instead of a float, or a null value) to an Apple Maps marker, the entire app will crash to the home screen without throwing a catchable JS error. Google Maps on Android usually just ignores the bad pin. And I spent two days tracking down a crash log only to realize a single API response had a latitude of null.

Where This is Heading

Looking at the current state of the repository issues and the general frustration around native map integrations, I probably expect the community to fully fork or replace the underlying iOS implementation by Q1 2027. The maintenance burden of keeping up with Apple’s MapKit changes while maintaining backward compatibility is clearly straining the current maintainers.

Mapbox is already eating a lot of their lunch in the React Native space, but their pricing model keeps smaller developers tied to react-native-maps.

And if you’re starting a new project today, I’d still stick with the library. Just remember to sanitize every single coordinate before it hits the map component, and for the love of god, turn off tracksViewChanges on your custom markers.