React native mapbox-gl crashes with react-navigation

Viewed 364

I have a problem. When I display a map with @rnmapbox/maps and that I want to navigate in my screens, the app freeze then crashes. I'm pretty sure that this is a rnmapbox problem because when I remove the code between <MapboxGL.MapView> and <MapboxGL.MapView/>, navigation works fine.

I use @react-navigation/native and @react-navigation/native-stack Note that this issus don't append on iOS, only on Android.

Record of the issue : https://vimeo.com/723749736

Here is my code :

App.js

import React from 'react';
import MapScreen from './src/screens/MapScreen';
import PlaceDetailsScreen from './src/screens/PlaceDetailsScreen';
import VideoPlayerScreen from './src/screens/VideoPlayerScreen';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {NavigationContainer} from '@react-navigation/native';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import {GestureHandlerRootView} from 'react-native-gesture-handler';

function App() {
  const Stack = createNativeStackNavigator();
  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={{flex: 1}}>
        <NavigationContainer>
          <Stack.Navigator>
            <Stack.Screen
              name="Map"
              component={MapScreen}
              options={{headerShown: false}}
            />
            <Stack.Group>
              <Stack.Screen name="Details" component={PlaceDetailsScreen} />
              <Stack.Screen
                name="VideoPlayer"
                component={VideoPlayerScreen}
                options={{headerShown: false}}
              />
            </Stack.Group>
          </Stack.Navigator>
        </NavigationContainer>
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
}

export default App;

MapScreen.js

import React, {useState, useRef, useMemo, useCallback, useEffect} from 'react';
import {View, Text, PermissionsAndroid, Platform} from 'react-native';
import MapboxGL from '@rnmapbox/maps';
import BottomSheet, {BottomSheetScrollView} from '@gorhom/bottom-sheet';
import Styles from '../../Styles';
import PlaceBtn from '../components/PlaceBtn/PlaceBtn';
import IconPin from './../assets/icons/icon-locationPin.svg';
MapboxGL.setAccessToken(
  'XXXXXXXXXXXXXXXXXXXXXXXXXXX',
);

function MapScreen({navigation}) {
  const [hasGeolocPermission, setHasGeolocPermission] = useState(false);
  const [currentRegion, setCurrentRegion] = useState([4.824, 45.76]); //longitude, latitude
  const [userLocation, setUserLocation] = useState();
  const [nearestPoints, setNearestPoints] = useState([]);
  const markers = require('../data/markers.json');
  const route = require('../data/route.json');
  const routeLine = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        properties: {},
        geometry: {
          type: 'LineString',
          coordinates: route.points,
        },
      },
    ],
  };

  const bottomSheetRef = useRef(null);
  const snapPoints = useMemo(() => ['13%', '75%', '100%'], []);

  const handleSheetChanges = useCallback(index => {
    console.log('handleSheetChanges', index);
  }, []);

  const requestLocationPermission = async () => {
    if (Platform.OS === 'android') {
      try {
        const granted = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
        );
        if (granted === PermissionsAndroid.RESULTS.GRANTED) {
          console.log('location uses is granted');
          setHasGeolocPermission(true);
        } else {
          console.log(granted);
          console.log('location access denied');
        }
      } catch (error) {
        console.warn('error while request location : ', error);
      }
    }
  };

  useEffect(() => {
    requestLocationPermission();
  }, []);
  return (
    <View style={Styles.container}>
      <MapboxGL.MapView style={Styles.map} scaleBarEnabled={false}>
        <MapboxGL.UserLocation
          animated
          showsUserHeadingIndicator
          minDisplacement={1}
          onUpdate={position => {
            console.log('position : ', position);
          }}
        />
        <MapboxGL.Camera centerCoordinate={currentRegion} zoomLevel={14} />
        {markers.map((marker, index) => {
          if (marker.category === 'introduction') {
            console.log('intro');
          } else {
            return (
              <MapboxGL.PointAnnotation
                key={index}
                id={marker.id + ''}
                coordinate={[marker.longitude, marker.latitude]}>
                <IconPin width={35} height={35} fill={'#00c0aa'} />
              </MapboxGL.PointAnnotation>
            );
          }
        })}
        <MapboxGL.ShapeSource id="line1" shape={routeLine}>
          <MapboxGL.LineLayer
            id="linelayer1"
            style={{lineColor: 'red', lineWidth: 5}}
          />
        </MapboxGL.ShapeSource>
      </MapboxGL.MapView>
      <BottomSheet
        ref={bottomSheetRef}
        handleIndicatorStyle={Styles.handleIndicator}
        index={0}
        snapPoints={snapPoints}
        onChange={handleSheetChanges}>
        <View style={Styles.bottomSheetContent}>
          <Text style={Styles.bottomSheetTitle}>{route.name}</Text>
          <BottomSheetScrollView>
            {markers.map((place, index) => {
              return (
                <PlaceBtn
                  place={place}
                  navigation={navigation}
                  //isNear={nearestPoints.includes(place.id)}
                />
              );
            })}
          </BottomSheetScrollView>
        </View>
      </BottomSheet>
    </View>
  );
}

export default MapScreen;

PlaceDetailsScreen.js

import React from 'react';
import {View, Text, TouchableOpacity} from 'react-native';

function PlaceDetailsScreen({navigation}) {
  return (
    <>
      <TouchableOpacity
        style={{backgroundColor: '#00FF00'}}
        onPress={() => navigation.navigate('Map')}>
        <Text>Go to MAP</Text>
      </TouchableOpacity>
      <TouchableOpacity
        style={{backgroundColor: '#00FF00'}}
        onPress={() => navigation.navigate('VideoPlayer')}>
        <Text>Go to Video Player</Text>
      </TouchableOpacity>
    </>
  );
}
```
1 Answers

I had the same problem. In my case I have a modal that is persistent through all screens and that modal shrinks when user swipes down like in youtube. After I initialized map inside modal, whenever I try to change screen my app were first freezing then crashing.

My solution was to render map only when modal is expanded. So I made a conditional rendering. Checked whether modal is shrinked or not. If shrinked, then render the map, else don't render. Thus my navigation worked without any problem.

This was my case and that's how I solved it:

 // I use reduxjs toolkit.
 // Since this modal is gonna be persistent through all screens
 // It's best to store translateY value in redux.
 const Modal = useAppSelector((state) => state.Modal);
 const translateY = useSharedValue(Modal.translateY);
 
 // I watch this variable with an useEffect hook to dynamically update redux value
 const [translateYState, setTranslateYState] = useState(translateY.value);

 useEffect(() => {
    dispatch(setTranslateY(translateYState));
  }, [translateYState]);

 // translateYState's value is dynamically updating by a pan gesture detector
 // translateYState value to be 0 means modal is expanded and user is using the modal
 return (
    <>
      {translateYState==0 && (
        <MapboxGL.MapView style={styles.map}></MapboxGL.MapView>
      )} 
    </>
   )

You can un-render the map when the modal which has title "Les Sens de Lyon" expands. If you are worried about your map data, you can store the map data for example all locations of markers in redux to be persistent but if your app is a single page app it should not be a problem else you can use redux.

Related