I'm trying to zoom-in on a set of coordinates using react-map-gl. To do so, the library encourages us to use React's Refs. When instantiating the component in render
, I'm passing down a ref that should hold the underlying MapBox map object. I managed to make the behavior work. Only problem: it's highly inconsistent.
I'm calling a method called setCamera
in componentDidUpdate
. But it only works on the initial load. If I reload the page, I've got an error from Gatsby. If I close the error it will work again. Up until the next reload.
The error is that this.mapRef.current
is null
. I tried to put a conditional to verify that the value isn't null
before trying to access it, however this cause the animation to just never work. No matter how many times I reload, it will never be performed. Whereas without the conditional it could at least work half the time before crashing. So this already is a mystery in itself and if someone has an idea for why such behavior can happen, I'm all ears.
Still, I wasn't discouraged and tried to put the call to setCamera
in a setTimeout
and yes, it works! Even putting a very low timeout like 1 makes the code work 95% of the time. But I'm unsatisfied with it, because I understand that putting that kind of timers isn't what I'm supposed to do and to make things worse, it doesn't fix the issue consistently.
My understanding of the problem is that MapRef is still not set in componentDidUpdate
for some reason. It's being set sometimes later. I don't know if React supports threading or if some kind of async witchcraft is deceiving me behind the scenes, but what I'm wondering is when am I guaranteed for my ref to be properly set? Where or how should I write this code?
Thank you in advance to anyone who can help me on that. 🙂
Here's my sample code:
import React, {Component} from 'react';
import MapGL, {NavigationControl, Source, Layer} from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import gpxParser from 'gpxparser';
import bbox from '@turf/bbox';
const MAPBOX_TOKEN = 'exampleToken'
class HikeMapbox extends Component {
constructor(props) {
super(props)
this.state = {
gpxData: '',
};
}
// Read et get the GPX file, return it as a bunch of text
componentDidMount() {
const gpxPath = `https:${this.props.gpxPath}`;
// Simple GET request using fetch
fetch(gpxPath)
.then(response => response.text())
.then(text => this.setState({ gpxData: text }));
}
componentDidUpdate() {
// Set the camera on didUpdate
setTimeout(() => {
const geoJsonData = this.getGeoJson();
this.setCamera(geoJsonData);
}, 10);
}
// Get Max and Min Lat and Lon to Zoom on the GPX trace
setCamera(geoJsonData) {
if (geoJsonData) {
const [minLng, minLat, maxLng, maxLat] = bbox(geoJsonData);
this.mapRef.current.fitBounds(
[
[minLng, minLat],
[maxLng, maxLat]
],
{padding: 40}
);
}
}
// Take the text and parse it as geoJSON
getGeoJson() {
const { gpxData } = this.state;
let gpx = new gpxParser();
try {
gpx.parse(gpxData);
} catch (err) {
console.log(err);
}
const geoJson = gpx.toGeoJSON();
return geoJson
}
// Return the GPX trace in Mapbox
showGpxFile() {
const GeoJsonData = this.getGeoJson();
// Choose the style of the GPX trace
const layerStyle = {
id:'contours',
type:'line',
source:'contours',
paint: {
'line-color': 'blue',
'line-width': 3
}
};
return (
<>
{
// Return the React Mapbox GL code to show the GPX file on the map
GeoJsonData && (
<Source type="geojson" data={GeoJsonData}>
<Layer {...layerStyle} />
</Source>
)
}
</>
)
}
render() {
this.mapRef = React.createRef();
return (
<>
<MapGL
ref={this.mapRef}
initialViewState={{
latitude: this.props.latitude,
longitude: this.props.longitude,
zoom: 8,
}}
style={{width: "100%", height: "100%"}}
mapStyle="mapbox://styles/mapbox/outdoors-v11"
mapboxAccessToken={MAPBOX_TOKEN}
>
<NavigationControl />
{this.showGpxFile()}
</MapGL>
</>
)
}
}
export default HikeMapbox;
By the way, I'm running this code on my computer using gatsby develop
. I don't know if that could be related, but I thought it could be relevant.
I've found a solution!
My issue was that setCamera
was dependent on two conditions and these two conditions could happen in any order.
Instead of initializing mapRef
, in the constructor or in render
, I've made a function…
onMapRefChange = node => {
this.setState({mapRef: node});
// I'm calling my method now
this.setCamera()
};
… That I'm passing in in the ref parameter
<MapGL
ref={this.onMapRefChange}
...
Eventually onMapRefChange
will receive an actual map object and then the code in setCamera
will be able to access it.