Esri / esri-leaflet

A lightweight set of tools for working with ArcGIS services in Leaflet. :rocket:
https://developers.arcgis.com/esri-leaflet/
Apache License 2.0
1.6k stars 798 forks source link

basemapLayer: Cannot set property 'innerHTML' of null in React app #945

Closed forgo closed 7 years ago

forgo commented 7 years ago

Chrome 57.0.2987.133 (64-bit), Safari 10.1 Using NPM/Node/Webpack and React:

"esri-leaflet": "^2.0.8", "leaflet": "^1.0.3",

The single line of code utilizing esri-leaflet is for sure the culprit, as removing it (and thus the baselayer of my maps) stops the error from popping up in the developer console in Chrome and Safari. Everything seems to be functioning alright, but my console is inundated with these errors, and I need to remove them to make this a production-ready application.

VM20550:819 Uncaught TypeError: Cannot set property 'innerHTML' of null
    at _updateMapAttribution (eval at <anonymous> (http://localhost:10099/bundle-993595c0a6d1bc0978ee.js:1:1381880), <anonymous>:819:35)
    at eval (eval at <anonymous> (http://localhost:10099/bundle-993595c0a6d1bc0978ee.js:1:1381880), <anonymous>:790:6)
    at Object.window._EsriLeafletCallbacks.(anonymous function) [as c3] (eval at <anonymous> (http://localhost:10099/bundle-993595c0a6d1bc0978ee.js:1:1381880), <anonymous>:202:17)
    at https://static.arcgis.com/attribution/World_Topo_Map?callback=window._EsriLeafletCallbacks.c3&f=json:1:30
_updateMapAttribution @ VM20550:819
(anonymous) @ VM20550:790
window._EsriLeafletCallbacks.(anonymous function) @ VM20550:202
(anonymous) @ World_Topo_Map?callback=window._EsriLeafletCallbacks.c3&f=json:1

Steps to reproduce the error:

  1. Create a map with leaflet in React component:

import L from 'leaflet' import E from 'esri-leaflet'

Offending line: // E.basemapLayer(esriBasemapLayer).addTo(this.map); where: esriBasemapLayer = "Topographic";

I took a lot of time to create a vanilla Node/Webpack/React project to reproduce this error as simply as possible, but unfortunately, I haven't been able to reproduce the problem in a stripped down way.

So I am resorting to showing the majority of my React component:

import React from 'react'
import ReactDOM from 'react-dom'
import L from 'leaflet'
import E from 'esri-leaflet'

export class Map extends React.Component {
    render() {
        const { esriBasemapLayer, geoJSONData, clickEventDetail, clickPictureDetail } = this.props;
        return (
            <div className={Styles.Map}>
                <div ref={() => this.renderMap(esriBasemapLayer, geoJSONData, clickEventDetail, clickPictureDetail)} />
            </div>
        )
    }
    renderMap(esriBasemapLayer, geoJSONData, clickEventDetail, clickPictureDetail) {
        if (this.map) {
            this.map.remove();
        }
        let mapContainer = ReactDOM.findDOMNode(this);

        // configure default marker map icons
        delete L.Icon.Default.prototype._getIconUrl;
        L.Icon.Default.mergeOptions({
            iconRetinaUrl: require("../img/leaflet/marker-icon-2x.png"),
            iconUrl: require("../img/leaflet/marker-icon.png"),
            shadowUrl: require("../img/leaflet/marker-shadow.png")
        });

        let eventMarkerIcon = L.icon({
            iconRetinaUrl: require("../img/leaflet/marker-event.png"),
            iconUrl: require("../img/leaflet/marker-event.png"),
            // shadowUrl: require("../img/leaflet/marker-event-shadow.png"),
            iconSize: [30, 27],   // size of the icon
            shadowSize: [30, 27], // size of the shadow
            iconAnchor: [15, 14], // point of the icon which will correspond to the marker's location
            shadowAnchor: [15, 14], // the same for the shadow
            popupAnchor: [0, -27], // point from which the popup should open relative to the iconAnchor
        });

        let pictureMarkerIcon = L.icon({
            iconRetinaUrl: require("../img/leaflet/marker-picture.png"),
            iconUrl: require("../img/leaflet/marker-picture.png"),
            // shadowUrl: require("../img/leaflet/marker-event-shadow.png"),
            iconSize: [32, 32],   // size of the icon
            shadowSize: [32, 32], // size of the shadow
            iconAnchor: [16, 16], // point of the icon which will correspond to the marker's location
            shadowAnchor: [16, 16], // the same for the shadow
            popupAnchor: [0, -32], // point from which the popup should open relative to the iconAnchor
        });

        this.map = L.map(mapContainer);

        // configure map control settings
        this.map.scrollWheelZoom.disable();
        this.map.keyboard.disable();

        // THIS LINE CAUSES THE ERROR
        E.basemapLayer(esriBasemapLayer).addTo(this.map);

        const geoJSON = L.geoJSON(geoJSONData, {
            pointToLayer: (feature, latlng) => {
                console.log("feature:", feature);
                if(feature.properties.name === "Event") {
                    return L.marker(latlng, {icon: eventMarkerIcon});
                }
                else if(feature.properties.name === "Picture") {
                    return L.marker(latlng, {icon: pictureMarkerIcon});
                }
                else {
                    return L.Icon.Default;
                }
            },
            onEachFeature: (feature, layer) => {
                console.log("EventDetail::onEachFeature");
                const popupImgSrc = feature.properties.imgSrc;
                const popupImg = "<img class=\""+ Styles.popupImg +"\" src=\""+popupImgSrc+"\"/>";
                const popupDescription = "<div class=\""+ Styles.popupDescription +"\">"+feature.properties.description+"</div>";
                const popup = "<div class=\""+ Styles.popup +"\">"+ popupImg + popupDescription +"</div>";

                layer.bindPopup(popup);
                layer.on('mouseover', () => { layer.openPopup() });
                layer.on('mouseout', () => { layer.closePopup() });
                layer.on('click', () => {
                    if(feature.properties.name === "Event") {
                        clickEventDetail(feature.properties.id);
                    }
                    else if(feature.properties.name === "Picture") {
                        clickPictureDetail(feature.properties.id);
                    }
                });
            }
        });
        MapUtils.setBoundsAndZoom(this.map, geoJSON);
        geoJSON.addTo(this.map);
    }
}

export default Map
jgravois commented 7 years ago

nothing jumps out at me looking at your code, so a simplified repro case is going to be necessary to trap the error.

i (and others) have definitely got our plugins running in React successfully...

forgo commented 7 years ago

I've been able to strip down to a more simplified reproduction of the error. I have moved the definition of leaflet and esri-leaflet into componentDidMount for reasons mentioned here.

Otherwise, I have removed any other customizations surrounding my marker placement, etc...

I am simply setting the coordinate/zoom and adding the baselayer. This is enough to reproduce 5 of the same error messages in my Javascript console mentioned above:

VM37861:819 Uncaught TypeError: Cannot set property 'innerHTML' of null
    at _updateMapAttribution (eval at <anonymous> (http://localhost:10099/bundle-d63c4b074c1ba077863d.js:1:2952079), <anonymous>:819:35)
    at eval (eval at <anonymous> (http://localhost:10099/bundle-d63c4b074c1ba077863d.js:1:2952079), <anonymous>:790:6)
    at Object.window._EsriLeafletCallbacks.(anonymous function) [as c1] (eval at <anonymous> (http://localhost:10099/bundle-d63c4b074c1ba077863d.js:1:2952079), <anonymous>:202:17)
    at https://static.arcgis.com/attribution/World_Topo_Map?callback=window._EsriLeafletCallbacks.c1&f=json:1:30

Component:

import React from 'react'
import ReactDOM from 'react-dom'
import Styles from './Map.css'

let L, E;

export class Map extends React.Component {

    componentDidMount() {
        // only runs on client, not on server render
        L = require("leaflet");
        E = require("esri-leaflet");
        this.forceUpdate();
    }

    render() {
        return (
            <div className={Styles.Map}>
                <div className={Styles.mapContainer} ref={() => this.renderMap()} /> 
            </div>
        )
    }
    renderMap() {
        if(!L || !E) {
            return;
        }
        if (this.map) {
            this.map.remove();
        }
        let mapContainer = ReactDOM.findDOMNode(this);
        this.map = L.map(mapContainer);
        this.map.setView([45, 137], 6);
        E.basemapLayer("Topographic").addTo(this.map);
    }
}

export default Map
jgravois commented 7 years ago

thanks for that. i'm juggling a couple other things at the moment, but i'll be able to dig in deeper in a couple days.

jgravois commented 7 years ago

i still can't reproduce this error.

// @1.0.3 / @2.0.8
import L from 'leaflet'
import esri from 'esri-leaflet'

// @15.5.4
import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class Map extends Component {
  constructor (props) {
    super(props)
  }

  render () {
    const style = { position: 'absolute', top:'0', bottom:'0', right:'0', left:'0' };

    return (
      <div style={style}>
          <div ref={() => this.renderMap()} />
      </div>
    )
  }

  renderMap() {
    let mapContainer = ReactDOM.findDOMNode(this);
    this.map = L.map(mapContainer);
    this.map.setView([45, 137], 6);
    esri.basemapLayer("Topographic").addTo(this.map);
  }
}

ReactDOM.render(<Map />, document.getElementById('react-app'))
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
    <title>github user map | react</title>
    <meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/leaflet/1.0.3/leaflet.css">
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
  <div id="react-app"></div>
  <script src="bundle.js"></script>
  </body>
</html>

putting that aside for a moment, in your app it appears we fail to introspect the DOM and set basemap attribution:

var attributionElement = map.attributionControl._container.querySelector('.esri-attributions');
attributionElement.innerHTML = newAttributions;

screenshot 2017-04-21 14 46 53

is that <span> actually drawing in your app?

jgravois commented 7 years ago

you ever track down the cause of your error @forgo?

forgo commented 7 years ago

Sorry it's taken me a while to respond. I have been busy working on my team's continuous integration environment. Now I've kind of circled back to this problem, and I would like to get rid of these errors. I will start diving in and let you know what I find. Thanks for looking into it so far.

forgo commented 7 years ago

I get the <span> you referred to earlier, but the span class is class="esri-dynamic-attribution" and not the class="esri-attributions" I see in your screenshot.

I noticed in your Util.js that it is looking for "esri-dynamic-attribution", and not the "esri-attributions" you showed in your code snippets above.

The Util.js under esri-leaflet/src in my node_modules appears to be looking for the right "esri-dynamic-attribution" as well, so I'm not sure this is the root of the issue or not.

forgo commented 7 years ago

I have a suspicion it might have something to do with the react ref={()=>this.renderMap(..)}

I noticed that renderMap is getting called twice, back-to-back sometimes. Hard to say if this is expected, but it doesn't really make sense to me, considering I only have 0 or 1 maps on a page at a time.

If you have a better way of rendering a map in a React component without using the "ref" attribute, I would am open to getting rid of it, as I was simply borrowing that method via another project my team works on, and I didn't really understand completely how the "ref" works in React.

forgo commented 7 years ago

Looks like I was on the right track:

I removed the ref={()=>this.renderMap(..)} from within the my Map React component's render method entirely.

Instead I called my renderMap function in componentDidUpdate instead, extracting what I needed out of my props there. This prevented the render from occuring twice as mentioned above and also removed the errors I was seeing:

componentDidUpdate() {
    const { esriBasemapLayer, geoJSONData, clickEventDetail, clickPictureDetail, addBreadcrumbFromMap } = this.props;
    this.renderMap(esriBasemapLayer, geoJSONData, clickEventDetail, clickPictureDetail, addBreadcrumbFromMap)
}

Hope this is helpful to others who may run into similar issues.

Thanks again for looking into this!

jgravois commented 7 years ago

no worries at all. thanks for taking the time to close the loop @forgo.