kostadinovstoil kostadinovstoil - 26 days ago 11
React JSX Question

Filter google maps custom overlays depending on whether they fall within the current map bounds

I am struggling to find a proper way of rendering custom google map markers (overlays) in a react-redux project. What I have is a page showing a map and a search box. When someone searches for a place and the place is found, this triggers the map idle event, then I update map bounds and searched place info and save them in redux store. And then fetch data with current map bounds and city name. When data arrives, filter the listings (The filtering will go to the backend in future, which means the server will send already filtered listing which fall in the current viewport) and render custom overlays for each listing on the map.

On map idle event:

1) update map bounds and searched place name

2) fetch some data (json format) from a server

3) filter listings so we can render only the ones whose position falls in the current viewport (map bounds)

4) render custom overlay for each listing

On every map idle event, which appears to happen after you zoom or pan on the map, the whole process of updaitng, fetching, filtering and rendering repeats.

What I have done so far is that the project is working till the point when React needs to determine which overlays should be removed and which one should be re-drawn.

Actually what is not working correctly now is when the visible listings array updates, React just removes the listings which are at the end of the array (from the last index to 0) and not the correct one (the one whose position is out of the viewport).

Also sometimes, if you have searched for a place already and played around with the map a little bit and try to search for another place, the new place overlays are not positioned correctly. Instead they are way off the map viewport.

I am relatively new with all Ract, Redux and Google Maps Api technologies so I am aware I might be doing something really stupid. I hope someone here will be able to point me in the right direction. I have searched the entire net and I could not find a proper answer. I have found some helpful info about how to create custom overlays and how to create react component for google map and I also know there a couple of good npm modules which can do the job I want (such as this one: react-google-maps and this one: google-map-react) but they are all having their own problems and they are too complex for what I am trying to achieve.

I am sorry for pasting all of the code here but I am not sure how to represent the whole project environment in a jsbin or similar code bin. Please let me know if I need to make such a code working example and I will try.

Here is the code I have right now. Of course this is just the part which is important for the problem:

The map component

import React, { PropTypes, Component } from 'react';
import SearchBar from '../SearchBar';
import OverlayViewComponent from '../OverlayViewComponent';
import OverlayViewContent from '../OverlayViewContent';
import mapOptions from './cfg';
import MapStyles from './map.scss';

const propTypes = {
getListings: PropTypes.func.isRequired,
updateMapState: PropTypes.func.isRequired,
visibleListings: PropTypes.array.isRequired,
};

class GoogleMap extends Component {

constructor() {
super();
this._onMapIdle = this._onMapIdle.bind(this);
this.onPlacesSearch = this.onPlacesSearch.bind(this);
}

_initMap(mapContainer) {

// Create a new map
this._map = new google.maps.Map(mapContainer, mapOptions);

this._bindOnMapIdleEvent();
};

_bindOnMapIdleEvent() {
// Attach idle event listener to the map
this._map.addListener('idle', this._onMapIdle);
}

_onMapIdle() {
const { updateMapState, getListings } = this.props;

if (this._searchedPlace) {
console.log('ON MAP IDLE');

let mapBounds = this._map.getBounds().toJSON();
updateMapState(mapBounds, this._searchedPlace);

getListings();
}
};

onPlacesSearch(searchedPlace) {

if (searchedPlace.name !== '' && searchedPlace.geometry !== null) {

// Clear out the old marker if present.
if (this._searchedPlaceMarker) {
this._searchedPlaceMarker.setMap(null);
this._searchedPlaceMarker = null;
}

let bounds = new google.maps.LatLngBounds();

// Create a marker for the searched place.
this._searchedPlaceMarker = new google.maps.Marker({
map: this._map,
title: searchedPlace.name,
position: searchedPlace.geometry.location
});

if (searchedPlace.geometry.viewport) {
// Only geocodes have viewport.
bounds.union(searchedPlace.geometry.viewport);
} else {
bounds.extend(searchedPlace.geometry.location);
}

// Save currently searchedPlace into the class local variable
this._searchedPlace = searchedPlace;

// Set map so it contains the searchedPlace marker (Ideally it should be only one)
this._map.fitBounds(bounds);

} else {
return;
}
}

componentDidMount() {
// When component is mounted, initialise the map
this._initMap(this._mapContainer);
};

shouldComponentUpdate(nextProps) {
if (nextProps.visibleListings.length == this.props.visibleListings.length) {
return false;
} else {
return true;
}
};

componentWillUnmount() {
google.maps.event.clearInstanceListeners(this._map);
};

render() {
console.log('GOOGLEMAP RENDER');
return (
<div id="mapContainer">
<div id="mapCanvas" ref={(mapContainer) => this._mapContainer = mapContainer}></div>
<SearchBar onPlacesSearch={this.onPlacesSearch} />
{
this.props.visibleListings.map((listing, index) => {
return (
<OverlayViewComponent key={index} mapInstance={this._map} position={listing.geo_tag}>
<OverlayViewContent listingData={listing} />
</OverlayViewComponent>
);
})
}
</div>
);
}
};

GoogleMap.propTypes = propTypes;

export default GoogleMap;


The OverlayView Component

import React, { PropTypes, Component } from 'react';
import OverlayView from './utils/overlayViewHelpers';

const propTypes = {
position: PropTypes.array.isRequired,
mapInstance: PropTypes.object.isRequired,
};

class OverlayViewComponent extends Component {

componentDidMount() {
this._overlayInstance = new OverlayView(this.props.children, this.props.position, this.props.mapInstance);
};

componentWillUnmount() {
this._overlayInstance.setMap(null);
};

render() {
return null;
}
};

OverlayViewComponent.propTypes = propTypes;

export default OverlayViewComponent;


The OverlayView class

import ReactDOM from 'react-dom';

const EL_WIDTH = 30;
const EL_HEIGHT = 35;

function OverlayView(element, position, map) {
this.overlayContent = element;
this.point = new google.maps.LatLng(position[0], position[1]);
this.setMap(map);
}

OverlayView.prototype = Object.create(new google.maps.OverlayView());
OverlayView.prototype.constructor = OverlayView;

OverlayView.prototype.onAdd = function() {
console.log('onAdd');

this.containerElement = document.createElement('div');
this.containerElement.style.position = 'absolute';
this.containerElement.style.width = EL_WIDTH + 'px';
this.containerElement.style.height = EL_HEIGHT + 'px';

let panes = this.getPanes();

panes.overlayMouseTarget.appendChild(this.containerElement);

ReactDOM.render(this.overlayContent, this.containerElement);
};

OverlayView.prototype.draw = function() {
console.log('draw');
if (this.containerElement) {
let projection = this.getProjection();
let projectedLatLng = projection.fromLatLngToDivPixel(this.point);
console.log(projectedLatLng);

this.containerElement.style.top = projectedLatLng.y - EL_HEIGHT + 'px';
this.containerElement.style.left = projectedLatLng.x - Math.floor(EL_WIDTH / 2) + 'px';
}

};

OverlayView.prototype.onRemove = function() {
console.log('onRemove');
let parentEl = this.containerElement.parentNode;
parentEl.removeChild(this.containerElement);
ReactDOM.unmountComponentAtNode(this.containerElement);
};

export default OverlayView;


The OverlayView Content component

import React, { PropTypes } from 'react';
import markerIcon from '../../../images/icon-marker.png';

const propTypes = {
listingData: PropTypes.object.isRequired,
};

const OverlayViewContent = (listingData) => {
console.log('OverlayViewContent render');
return (
<div className="customIcon">
<img src={markerIcon} title={listingData.where} />
</div>
);
};

OverlayViewContent.propTypes = propTypes;

export default OverlayViewContent;

Answer

I think I have found the root of the problem.

Thanks to this post, and more specifically the comment from Sebastien Lorber. He made me take a more thourough look at the react documentation about the keys attributes

It appears that if I add a more unique key to the react components when I am looping trough the visibleListings array, everything works as expected. The problem was that i was using array indexes for the components keys. This way when the visibleListings array updates, the key for the overlay component which need to be unmount is removed from the array and react does not know which component should be unmount. Thus react always remove the last overlay component from the array.

Comments