2

I need to do create a Info Window for each Marker I have in my map. I can see the markers on the map, but when I click over the marker to get the InfoWindow, I get this error TypeError: Cannot read property 'isOpen' of undefined, referencing to this.setState({isOpen: !this.state.isOpen}) in the onToggleOpen.

Thanks on behalf

componentWillMount() {
    this.setState({ markers: [], isOpen: false })
  }

componentDidMount() {
    let list = { restaurants : []}
    database.ref('..').on('value', .. => {
            ...
            })
            this.setState({ markers:  list.restaurants});
        })

}

onToggleOpen(){
    this.setState({isOpen: !this.state.isOpen})
}


render() { 
    const MapWithAMarker = compose(
    withProps({
        googleMapURL: ...,
        loadingElement: ...,
        containerElement: ...,
        mapElement: ...
      }),
      withScriptjs,
      withGoogleMap
    )(props =>
      <GoogleMap
        defaultZoom={17}
        defaultCenter={{ lat: ..., lng: ... }}
      >
          {props.markers.map(marker => (
            <Marker
              key={marker.name}
              position={{ lat: marker.latitude, lng: marker.longitude }}
              onClick={this.onToggleOpen}
            >
            {this.state.isOpen && <InfoWindow onCloseClick={this.onToggleOpen}> marker.key </InfoWindow>}
            </Marker>
          ))}


      </GoogleMap>
    );
    return (
        <section id="mapa">
            <div class= "container">
                <div class="row">
                    <div class="col-md-12 text-left">
                        <h2 class="section-heading">MAPA</h2>

                        <MapWithAMarker markers={this.state.markers}/>

                    </div>
                </div>
                <div class="row text-center">

                </div>
            </div>
        </section>
    ) 
}

EDIT: Updated code:

export default class Mapa extends Component {
  constructor(props) {
    super(props)
    this.state = {
      markers: [],
      isOpen: false
    }
  }

componentDidMount() {
    let list = { restaurants : []}
    database.ref('..').on('value', restaurants => {
            restaurants.forEach( restaurant => {
                    list.restaurants.push({'name': restaurant.key,
                            'longitude': restaurant.val().lng,
                            'latitude': restaurant.val().lat}

                        )
            })
            this.setState({ markers:  list.restaurants });
        })

}

onToggleOpen = () => {
        this.setState({isOpen: !this.state.isOpen})
    }

render() { 

    const MapWithAMarker = compose(
    withProps({
        googleMapURL: "https://maps.googleapis.com/maps/api/js?key=...&v=3.exp&libraries=geometry,drawing,places",
        loadingElement: <div style={{ height: `100%` }} />,
        containerElement: <div style={{ height: `400px` }} />,
        mapElement: <div style={{ height: `100%` }} />
      }),
      withScriptjs,
      withGoogleMap
    )(props =>
      <GoogleMap
        defaultZoom={17}
        defaultCenter={{ lat: .., lng: ... }}
      >
          {props.markers.map(marker => (
            <Marker
              key={marker.name}
              position={{ lat: marker.latitude, lng: marker.longitude }}
              onClick={this.onToggleOpen}
            >
            {this.state.isOpen && <InfoWindow onCloseClick={this.onToggleOpen}> marker.key </InfoWindow>}
            </Marker>
          ))}

      </GoogleMap>
    );
    return (
        <section id="mapa">
            <div class= "container">
            <h3 class="info-title"><span class="info-title-span">Tasty and Convenient.</span></h3>
                <div class="row">
                    <div class="col-md-12 text-left">


                        <MapWithAMarker markers={this.state.markers}/>

                    </div>
                </div>
                <div class="row text-center">

                </div>
            </div>
        </section>
    ) 
}
L.Rencoret
  • 155
  • 2
  • 12

2 Answers2

5

The error indicates that this.state == undefined within the scope of where you are trying to retrieve this.state.isOpen.

This is because your this.onToggleOpen() function's scope is not properly bound to your component.

Try refactoring it to use an arrow function like so:

onToggleOpen = () => {
    this.setState({isOpen: !this.state.isOpen})
}

Side note:

Best practice to declare initial this.state is to do so within your component's constructor lifecycle method like so:

constructor(props) {
  super(props)
  this.state = {
    markers: [],
    isOpen: false
  }
}

Also, should this.state.isOpen be a unique property for of each of your markers instead of a blanket value for all of them? So that you can open each marker individually, instead of all at once.

Example Solution:

// Dependencies.
import React from 'react'
// GoogleMap, etc ..

// Map.
export default class Map extends React.Component {

  // State.
  state = {markers: []}

  // Render.
  render() {

    // Map With A Marker.
    const MapWithAMarker = compose(
      withProps({
        googleMapURL: "https://maps.googleapis.com/maps/api/js?key=...&v=3.exp&libraries=geometry,drawing,places",
        loadingElement: <div style={{height: `100%`}}/>,
        containerElement: <div style={{height: `400px`}}/>,
        mapElement: <div style={{height: `100%`}}/>
      }),
      withScriptjs,
      withGoogleMap
    )(props =>
      <GoogleMap defaultZoom={props.zoom} defaultCenter={{lat: props.lat, lng: props.lng}}>
        {props.markers.map(props => <RestaurantMarker key={props.name} {...props}/>)}
      </GoogleMap>
    )

    // Return Map.
    return (
      <section id="map">
        <div class= "container">
        <h3 class="info-title"><span class="info-title-span">Tasty and Convenient.</span></h3>
          <div class="row">
            <div class="col-md-12 text-left">
              <MapWithAMarker 
                markers={this.state.markers}
                lat={37.7749}
                lng={122.4194}
                zoom={17}
              />
            </div>
          </div>
          <div class="row text-center">
            Text
          </div>
        </div>
      </section>
    )

  }

  // Did Mount.
  componentDidMount() {

    // Download Restaurants.
    database.ref('..').on('value', restaurants => {

      // Restaurants Placeholder.
      let markers = []

      // Map Restaurants To List.
      restaurants.forEach(restaurant => {
        markers.push({
          'name': restaurant.key,
          'longitude': restaurant.val().lng,
          'latitude': restaurant.val().lat
        })
      })

      // Update State.
      this.setState({markers})

    })

  }

}


// RestaurantMarker.
class RestaurantMarker extends React.Component {

  // State.
  state = {open: false}

  // Render.
  render() {

    // Extract Props.
    const {name, latitude, longitude} = this.props

    // Return Restaurant Marker Component.
    return (
      <Marker key={name} position={{ lat: latitude, lng: longitude }}>
        {this.state.open && (
          <InfoWindow onClick={() => this.setState(state => ({open: !state.open}))}> {name} </InfoWindow>
        )}
      </Marker>
    )

  }

}
Arman Charan
  • 5,669
  • 2
  • 22
  • 32
  • How could I make a property for each of the markers? Definitely I just want the info window for the marker I clicked.. Maybe passing the `isOpen` through props? – L.Rencoret Oct 23 '17 at 04:23
  • I would insert logic to check for an `isOpen` property for each of the `this.state.markers` children when rendering. ie. within your `props.markers.map()` function: `if (marker.isOpen) { // return open version of marker }` – Arman Charan Oct 23 '17 at 06:19
  • I couldn't make it work... I tried to add `isOpen` in the `list.restaurants.push({..., isOpen: false})` and later `{marker.isOpen && marker.key }`, but I don't know what to do on `onToggleOpen` function so I can change that props... – L.Rencoret Oct 23 '17 at 15:49
  • https://tomchentw.github.io/react-google-maps/#infowindow In that example they made it through props? I can't find a way to make it work – L.Rencoret Oct 23 '17 at 15:50
  • You probs don't want to pollute your Firebase data with idiosyncratic UI state references such was what's open in your map. To keep things simple and segmented, try managing `isOpen` as it's own `Object {marker.id: bool}` in `this.state` and passing it as a prop in your `render()` function to your `MapWithAMarker` component. ie ``. That way you can manage what's open individually by a relevant unique id such as `marker.name` based on what `this.state.isOpen[marker.name]` returns. – Arman Charan Oct 24 '17 at 02:52
  • The example you linked returns one single marker. Therefore, a blanket `isOpen` value is appropriate for their use case. There are a couple other solutions you could adopt such nesting all of the `{content}` logic that you return from `this.markers.map()` within it's own component class and returning that. You could then manage all of the idiosyncratic state independently from within each component you return. I would personally head in that sort of direction. – Arman Charan Oct 24 '17 at 03:02
  • I just updated my code under the EDIT:. I tried to do what you suggested but I couldn't make it work. Could you post your suggestion? Thanks – L.Rencoret Oct 24 '17 at 20:43
  • Goood idea. I revised my answer to include an example of the sort of solution I'm talking about. Note the `` component and logic therein to handle whether or not each marker is open in isolation. – Arman Charan Oct 25 '17 at 00:57
  • Thanks for the update! I'm having this small issue: Warning: Each child in an array or iterator should have a unique "key" prop. Check the top-level render call using ``. See react-warnings-keys for more information. in RestaurantMarker – L.Rencoret Oct 25 '17 at 16:31
  • My bad. React expects to see `keys` when handling `lists` of components. Simply add a key prop and pass a unique value to it, ie. restaurant.name. See [this tut](https://reactjs.org/docs/lists-and-keys.html) for more info. I've revised the code above to include a unique `key` prop for each ``. – Arman Charan Oct 26 '17 at 01:04
  • as well you have to exclude the key prop in here: `// Extract Props. const { props: { name, latitude, longitude, key } } = this` The `onClick` event was on the `infoWindow`, I changed that to the `Marker`, and added `onCloseClick` to the `infoWindow`. Thank you for all the help!! really appreciate it – L.Rencoret Oct 26 '17 at 20:10
  • I know this question is long answered, but the answer really helped me, particularly the example of ``, however the 'extract props' line is really confusing me - I can't quite understand what is happening. Is that just a lot of ES6 destructuring going on there? If so, can you possibly show an ES5 version? I love that it works, but I just can't quite grok why it does. Is that just creating a constant of `{name}` from that marker instance's props.name? What is the `= this` all about? Is that so the marker retains its individual properties (like 'name') when 'clicked' later? – Daydream Nation Jul 08 '18 at 14:58
  • Correct. See above for simplified solution @DaydreamNation – Arman Charan Jul 08 '18 at 22:17
  • [`Destructuring assignment`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) is common. My style, back then, was to just pull everything from [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) in one go. I've since found that `nested destructuring assignment` isn't that nice to look at / confuses other devs. The revised approach is definitely superior @DaydreamNation – Arman Charan Jul 09 '18 at 00:26
0

I did this in the main wrapper

withStateHandlers(() => ({
    isOpen: {},
  }), {
    onToggleOpen: ({ isOpen }) => (id) => ({
      isOpen: {
        ...isOpen,
        [id]:isOpen[id]==undefined?true:!isOpen[id]
      },
    })
  }),

And this on Marker component

{props.servicePoints.map((x) =>
        <Marker
          onClick={()=>{
            props.onToggleOpen(x.id);
          }}
          animation={google.maps.Animation.DROP}
          key={x.id}
          icon={"favicon.ico"}
          position={{ lat: x.latitude, lng: x.longitude }}
        >
          {props.isOpen[x.id]===true &&
            <InfoWindow
              key={x.id}
              options={{ closeBoxURL: ``, enableEventPropagation: true }}
              onCloseClick={()=>{
                props.onToggleOpen(x.id);
              }}>
              <div>
                <label>Nome:{x.name}</label><br></br>
                <label>Descrizione:{x.description}</label><br></br>
                <label>Indirizzo:{x.address}</label>
              </div>
            </InfoWindow>}}
         </Marker>
      )}

All is working fine. So if I click one Marker, only one InfoWindow appear.

Fabien Sartori
  • 235
  • 3
  • 10