reactjsreact-nativereact-native-iosreact-native-maps

React Native: How to stop map markers from re-rendering on every state update


I have a component that has a map with multiple custom markers for various locations and a carousel with cards for those same locations. When a user presses a marker, it should show the callout and show the location's name next to the marker (but outside of the callout).

However, because I update the state in onRegionChangeComplete, if the user moves the map and then quickly presses the marker (before the state finishes updating from calling setState in onRegionChangeComplete), then the markers will re-render before firing the onPress event, and the event is never fired.

One solution might be to use shouldComponentUpdate, however, the docs state that it should only be used for performance optimization and not to prevent re-renders (https://reactjs.org/docs/react-component.html#shouldcomponentupdate), but more importantly, my componentDidUpdate function has some conditional logic dependent on the region set in shouldComponentUpdate, as well as other conditional actions, so I don't want to prevent re-rendering of the entire component, just unnecessary re-rendering of the markers.

I'm also using the performance optimization mentioned in https://github.com/react-native-community/react-native-maps/issues/2082 of wrapping the makers in a component that implements shouldComponentUpdate and getDerivedStateFromProps, however, I'm not entirely sure this is doing anything because it seems like the parent component is just recreating all of my optimized markers rather than using their optimizations to handle re-rendering. Also, even if I don't use a wrapped marker but a conventional custom marker, I still have the same issues.

I've also opened an issue for this on react-native-maps but haven't gotten a response yet: https://github.com/react-native-community/react-native-maps/issues/2860

My 'onRegionComplete' function that updates state when map is moved. I removed a few other conditional state updates for brevity:

onRegionChangeComplete = (region) => {
    const nextState = { };
    nextState.region = region;

    if (this.state.showNoResultsCard) {
      nextState.showNoResultsCard = false;
    }

    .
    .
    .

    this.setState({ ...nextState });

    this.props.setSearchRect({
      latitude1: region.latitude + (region.latitudeDelta / 2),
      longitude1: region.longitude + (region.longitudeDelta / 2),
      latitude2: region.latitude - (region.latitudeDelta / 2),
      longitude2: region.longitude - (region.longitudeDelta / 2)
    });
  }

MapView using the more conventional marker (not the optomized version):

<MapView // show if loaded or show a message asking for location
    provider={PROVIDER_GOOGLE}
    style={{ flex: 1, minHeight: 200, minWidth: 200 }}
    initialRegion={constants.initialRegion}
    ref={this.mapRef}
    onRegionChange={this.onRegionChange}
    onRegionChangeComplete={this.onRegionChangeComplete}
    showsUserLocationButton={false}
    showsPointsOfInterest={false}
    showsCompass={false}
    moveOnMarkerPress={false}
    onMapReady={this.onMapReady}
    customMapStyle={mapStyle}
    zoomTapEnabled={false}
    >

        {this.state.isMapReady && this.props.places.map((place, index) => {
            const calloutText = this.getDealText(place, 'callout');
            return (
                <Marker
                    tracksViewChanges
                    key={Shortid.generate()}
                    ref={(ref) => { this.markers[index] = ref; }}
                    coordinate={{
                        latitude: place.getLatitude(),
                        longitude: place.getLongitude()
                    }}
                    onPress={() => { this.onMarkerSelect(index); }}
                    anchor={{ x: 0.05, y: 0.9 }}
                    centerOffset={{ x: 400, y: -60 }}
                    calloutOffset={{ x: 8, y: 0 }}
                    calloutAnchor={{ x: 0.075, y: 0 }}
                    image={require('../../Assets/icons8-marker-80.png')}
                    style={index === this.state.scrollIndex ? { zIndex: 2 } : null}
                >
               {this.state.scrollIndex === index &&
                    <Text style={styles.markerTitle}>{place.getName()}</Text>}

                  <Callout onPress={() => this.onCalloutTap(place)} tooltip={false}>
                    <View style={{
                      borderColor: red,
                      width: 240,
                      borderWidth: 0,
                      borderRadius: 20,
                      paddingHorizontal: 8,
                      flexDirection: 'column',
                      justifyContent: 'flex-start',
                      alignItems: 'center'
                    }}
                    >
                      <Text style={styles.Title}>Now:</Text>
                      <View style={{
                        width: 240,
                        flexDirection: 'column',
                        justifyContent: 'space-evenly',
                        alignItems: 'flex-start',
                        paddingHorizontal: 8,
                        flex: 1
                      }}
                      >
                        {calloutText.Text}
                    </View>
                </View>
            </Callout>
        </Marker>
        );
    }) 
}

</MapView>

My function for the marker's on press event:

onMarkerSelect(index) {
    this.setState({ scrollIndex: index });
    this.carousel._component.scrollToIndex({
      index,
      animated: true,
      viewOffset: 0,
      viewPosition: 0.5
    });

    this.markers[index].redrawCallout();
}

Updating state and then quickly pressing a marker will cause the onPress event not to fire. Also, the markers are re-rendered/recreated every time a parent component is updated. (I say recreated because it seems like the markers are re-rendering without even firing shouldComponentUpdate or componentDidUpdate).

Is there any way to update state in onRegionChangeComplete without forcing the markers to re-render?


Solution

  • For anyone else who happens to have this problem, the issue was that I was randomly generating the keys for the markers, causing the parent component to create new markers each time it was re-rendered.

    Specifically, the line key={Shortid.generate()} was the problem.