Search code examples
javascriptreact-nativegoogle-mapsreact-native-maps

React Native Maps Callout Rendering


I am using react-native-maps to display markers for train stations in my area. Each marker has a Callout with real time data of approaching trains.

The issue is; every callout is being rendered in the background for every marker I have on the map. Also each callout is being re-rendered as I have new data from real time API. This is causing hundreds of views being rendered even though I only need the callout of the marker that is pressed.app screenshot

Is there a way to make sure no callout is being rendered until user presses on a specific marker? After the press; I also want to make sure only that specific marker's callout is being rendered and displayed.

My code:

MapScreen:

const MapScreen = props => {
  // get user location from Redux store
  // this is used to center the map
  const { latitude, longitude } = useSelector(state => state.location.coords)

  // The MapView and Markers are static
  // We only need to update Marker callouts after fetching data
  return(
    <SafeAreaView style={{flex: 1}}>
    <MapView
        style={{flex: 1}}
        initialRegion={{
          latitude:  parseFloat(latitude) || 37.792874,
          longitude: parseFloat(longitude) || -122.39703,
          latitudeDelta: 0.06,
          longitudeDelta: 0.06
        }}
        provider={"google"}
      >
        <Markers />
      </MapView>
      </SafeAreaView>
  )
}

export default MapScreen

Markers component:

const Markers = props => {
  const stationData = useSelector(state => state.stationData)

  return stationData.map((station, index) => {
    return (
      <MapView.Marker
        key={index}
        coordinate={{
          // receives station latitude and longitude from stationDetails.js
          latitude: parseFloat(stationDetails[station.abbr].gtfs_latitude),
          longitude: parseFloat(stationDetails[station.abbr].gtfs_longitude)
        }}
        image={stationLogo}
        zIndex={100}
        tracksInfoWindowChanges={true}
      >
        <MapView.Callout
          key={index}
          tooltip={true}
          style={{ backgroundColor: "#ffffff" }}
        >
          <View style={styles.calloutHeader}>
            <Text style={{ fontWeight: "bold" }}>{station.name}</Text>
          </View>
          <View style={styles.calloutContent}>
            <StationCallout key={index} station={stationData[index]} />
          </View>
        </MapView.Callout>
      </MapView.Marker>
    );
  });
};

StationCallout component:

const StationCallout = (props) => {
  return(
    props.station.etd.map((route, index) => {
      const approachingTrains = function() {
        trainText = `${route.destination} in`;

        route.estimate.map((train, index) => {
          if (index === 0) {
            if (train.minutes === "Leaving") {
              trainText += ` 0`;
            } else {
              trainText += ` ${train.minutes}`;
            }
          } else {
            if (train.minutes === "Leaving") {
              trainText += `, 0`;
            } else {
              trainText += `, ${train.minutes}`;
            }
          }
        });

        trainText += " mins";

        return <Text>{trainText}</Text>;
      };

      return <View key={index}>
      {approachingTrains()}
      </View>;
    })
  )
};

export default StationCallout

Solution

  • I actually found the answer myself. I have created a reference to each marker, then passed an onPress property to <MapView.Marker> and showCallout property to its Callout component.

    Markers component:

    export default function Markers() {
      const {
        stations: { station }
      } = require("../../bartData/stations");
    
      const [clickedMarkerRef, setClickedMarkerRef] = useState(null)
    
      return station.map((trainStation, index) => {
        return (
          <MapView.Marker
            key={trainStation.abbr}
            coordinate={{
              latitude: parseFloat(trainStation.gtfs_latitude),
              longitude: parseFloat(trainStation.gtfs_longitude)
            }}
            image={Platform.OS === "ios" ? station_ios : station_android}
            zIndex={100}
            tracksInfoWindowChanges={true}
            onPress={() => setClickedMarkerRef(index)}
          >
            <CalloutContainer
              key={trainStation.abbr}
              stationName={trainStation.name}
              stationAbbr={trainStation.abbr}
              showCallOut={clickedMarkerRef === index}
            />
          </MapView.Marker>
        );
      });
    }
    

    And Callout component only fetches data when showCallOut is true. In Callout component

    useEffect(() => {
        if (props.showCallOut === true) {
          fetchTrainDepartures();
    
          const intervalId = setInterval(fetchTrainDepartures, 10000);
          return () => clearInterval(intervalId);
        }
      }, []);
    

    So, unless you click on a marker, the local state stays at null and callouts doesn't fetch any data.

    When you click on marker at index 0:

    • clickedMarkerRef is now 0.
    • showCallout is true => {clickMarkerRef === index}
    • on Callout.js file under useEffect hook => props.showCallout is true.
    • Data is fetched only for this callout.