I am trying to update my parent compenent's state according to child's component input value and render components again.
I have App, Map and ListPlaces components.
Map component shows the map and markers and takes markers from App component as a prop.
ListPlaces component has own state(searchQuery) and a method(updateQuery). Basically it uses regexp and filter the results according to the input and show filtered values in the list element. It's fine, however, I need much more than this. What I want to do here? According to input value changes, I want to update my markers (state in App component) and re-render my map component again to show only related markers on the map. Idea is basic, but I couldn't implement it.
When I try to change my markers which is actually array, it would be 0. I will share my all code but I need to say that I think the problem probably is about onChange method in the input element (ListPlaces)
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
places: [],
markers: [],
markerID: -1,
newmarkers: []
};
this.changeMarkers = this.changeMarkers.bind(this);
}
componentDidMount() {
fetch(
"api_url"
)
.then(response => response.json())
.then(data => {
this.setState({
places: data.response.venues,
markers: data.response.venues
});
})
.catch(error => {
console.log("Someting went wrong ", error);
});
}
openInfo = (e, id) => {
this.setState({
markerID: id
});
};
closeInfo = () => {
this.setState({
markerID: -1
});
};
changeMarkers = newValue => {
const newmarkers = this.state.places.filter(
place => place.name === newValue
);
this.setState({
markers: newmarkers
});
};
toggleListPlaces = () => {
const nav = document.getElementById("nav-toggle");
const body = document.getElementsByTagName("body")[0];
nav.classList.toggle("active");
if (body.classList.contains("show-nav")) {
body.classList.remove("show-nav");
} else {
// If sidebar is hidden:
body.classList.add("show-nav");
}
};
render() {
return (
<div className="App">
<Map
role="application"
places={this.state.places}
markers={this.state.markers}
openInfoHandler={this.openInfo}
closeInfoHandler={this.closeInfo}
markerID={this.state.markerID}
googleMapURL="map_url"
loadingElement={<div style={{ height: "100%" }} />}
containerElement={<div style={{ height: "100%" }} />}
mapElement={<div style={{ height: "100%" }} />}
/>
<ListPlaces
toggleListHandler={this.toggleListPlaces}
locations={this.state.places}
openInfoHandler={this.openInfo}
changeMarkersHandler={this.changeMarkers}
/>
</div>
);
}
}
export default App;
ListPlaces
class ListPlaces extends Component {
state = {
searchQuery: ""
};
updateQuery = query => {
this.setState({ searchQuery: query});
};
render() {
const { toggleListHandler, locations, openInfoHandler, changeMarkersHandler} = this.props;
let showLocations;
if (this.state.searchQuery) {
const match = new RegExp(escapeRegExp(this.state.searchQuery), "i");
showLocations = locations.filter(location =>match.test(location.name));
} else {
showLocations = locations;
}
return (
<div>
<aside>
<h2>Restaurants</h2>
<nav>
<div className="search-area">
<input
className="search-input"
type="text"
placeholder="Search Restaurant"
value={this.state.searchQuery}
onChange={e => {this.updateQuery(e.target.value); changeMarkersHandler(e.target.value)}}
/>
</div>
<ul>
{showLocations.map(location => {
return (
<li
key={location.id}
onClick={e =>
openInfoHandler(e, location.id)
}
>
{location.name}
</li>
);
})}
</ul>
</nav>
<p>Information provided by Foursquare</p>
</aside>
<a
onClick={toggleListHandler}
id="nav-toggle"
className="position"
>
<span />
</a>
</div>
);
}
}
export default ListPlaces;
Map
const Map = withScriptjs(withGoogleMap((props) =>
<GoogleMap
defaultZoom={14}
defaultCenter={{ lat: 48.2854790, lng: -143.1407394 }}
>
{props.markers.map(restaurant => {
let marker = (
<Marker
id={restaurant.id}
key={restaurant.id}
name={restaurant.name}
position={{lat: restaurant.location.lat, lng: restaurant.location.lng}}
address={restaurant.location.address}
defaultAnimation={window.google.maps.Animation.DROP} // Should be 1 or 2 according to Stackoverflow
onClick={e => {props.openInfoHandler(e, restaurant.id)}}
animation = {props.markerID === restaurant.id && window.google.maps.Animation.BOUNCE}
>
{props.markerID === restaurant.id && (
<InfoWindow onCloseClick={e => {props.closeInfoHandler()}}>
<div className="info-window">
<p className="restaurant-name">{restaurant.name}</p>
<p className="restaurant-address">{restaurant.location.address}</p>
</div>
</InfoWindow>
)}
</Marker>
);
return marker;
})}
</GoogleMap>
))
export default Map;
you're on the right track - you need to lift up the filter value/filter operation into the common parent
App.js
class App extends React.Component {
onChangeFilter = (newFilter) => {
this.setState({ filter: newFilter })
}
render () {
const { filter, places } = this.state
const mPlaces = places.filter(/* filter places here */)
return (
<div className="App">
<ListPlaces
places={ mPlaces }
onChangeFilter={ this.onChangeFilter }
...
/>
<Map
locations={ mPlaces }
...
/>
</div>
)
}
}
ListPlaces.js
class ListPlaces extends React.Component {
updateQuery = (query) => {
this.props.onChangeFilter(query)
}
...
}
this is a common pattern in React - so common it has its own page in the React docs.
Related
I am tickling with Algolia autocomplete, and I am trying to replicate their custom renderer in react using the class component. This is the sandbox of the minimal demo of custom renderer using functional component,
and here is my attempt to convert it into a class component.
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.inputRef = React.createRef();
this.autocomplete = null;
this.state = {
autocompleteState: {},
};
}
componentDidMount() {
if (!this.inputRef.current) {
return undefined;
}
this.autocomplete = createAutocomplete({
onStateChange({ state }) {
// (2) Synchronize the Autocomplete state with the React state.
this.setState({ autocompleteState: state });
},
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
},
},
],
});
},
getItemUrl({ item }) {
return item.url;
},
},
];
},
});
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.inputRef.current,
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
ref={this.inputRef}
{...this.autocomplete?.getInputProps({})}
/>componentDidUpdate()
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source,
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
and the sandbox of the same version, I also tried using componentDidUpdate() but no luck, any lead where I did wrong would be much appreciated thank you :)
Ok, dont know why you need it made into class component but here you go:
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
autocompleteState: {},
query: '',
};
this.autocomplete = createAutocomplete({
onStateChange: this.onChange,
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
console.log('getting query', query)
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>"
}
}
]
});
},
getItemUrl({ item }) {
return item.url;
}
}
];
}
});
}
onChange = ({ state }) => {
console.log(state)
this.setState({ autocompleteState: state, query: state.query });
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.state.query
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
value={this.state.query}
{...this.autocomplete?.getInputProps({})}
/>
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
Anyway the componentDidMount is called only once, and because of ref object is undefined it just returned from it.
Also messing with this in class components is quite a bad idea (that is why func components are recommended)
I have a main component defined as App.js
import "./styles.css";
import { Component } from "react";
import Item from "./components/Item";
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
const list = state.items.push(state.textInput);
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = [...state.items];
list.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
and a child Component defined in Item.js
import { Component } from "react";
class Item extends Component {
render() {
return (
<div>
<span>{this.props.name}</span>
<span onClick={this.props.removeItem}>X</span>
</div>
);
}
}
export default Item;
Now my UI looks like
In the above code(App.js) Iam trying to iterate the items and then display the names using the child component. But due to some reason, on entering the text in the input and clicking add its not showing up. Also there are no errors in the console.
Please help, thanks in advance
Edited:
After the recent changes I get this error
You do not need to call this.deleteItem(idx) while passing it to the child.
import "./styles.css";
import { Component } from "react";
import Item from "./components/Item";
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
const list = state.items.push(state.textInput);
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = state.items.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
Updating state in react requires a new reference to objects.You're using Array#push. It will not detect your new change and the DOM will not update. You need to return a new reference.
insertItem() {
if (this.textInput === "") {
this.setState((state) => {
// const list = state.items.push(state.textInput);
const list = [...state.items, state.textInput];
return {
list,
textInput: ""
};
});
}
}
In order to track the array, you must add the key attribute:
{this.state.items.map((item, idx) => {
return <Item key={idx} name={item} removeItem={this.deleteItem(idx)} />;
})}
Here I used the index, but it would be better to use some ID of your model.
UPDATE:
I'd move the handler binding in the constructor:
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
this.insertItem = this.insertItem.bind(this);
}
Then:
<button onClick={this.insertItem}>Add</button>
UPDATE 2:
Okay, it seems you have several mistakes (and I didn't notice them at first glance).
Here is the complete working source (tested):
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
//const list = state.items.push(state.textInput);
const list = [...state.items, state.textInput];
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = [...state.items];
list.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item key={idx} name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
I think you should move display of template into return statement rather than inside render
...
render() {
return (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
}
I'm trying to get some map over some data and render it as a card style component.
The data structure is as follows within my user array (all fake data)
and this is the code...
import React from "react";
import OverlayContent from "./OverlayContent";
import { onCountryClick } from "../Scene3D/AppSignals";
import Portal from "./Portal";
import "./style.scss";
class Overlay extends React.Component {
constructor(props) {
super(props);
this.state = { overlay: false, users: [], country: [] };
this.openOverlay = this.openOverlay.bind(this);
this.closeOverlay = this.closeOverlay.bind(this);
}
openOverlay() {
this.setState({ overlay: true });
}
closeOverlay() {
this.setState({ overlay: false });
}
onCountryClick = (country, users) => {
this.openOverlay();
this.setState({ users: [users], country });
};
componentDidMount() {
this.onCountrySignal = onCountryClick.add(this.onCountryClick);
}
componentWillUnmount() {
this.onCountrySignal.detach();
}
render() {
const country = this.state.country;
const users = this.state.users;
console.log(users);
return (
<div className="btn-container">
{this.state.overlay && (
<Portal>
<div>
<h1>{country}</h1>
{users &&
users.map(user => (
<div className="user_container">
<h1 key={user.id} className="user_name">
{user.favouriteQuote}
</h1>
</div>
))}
<button className="btn" onClick={this.closeOverlay}>
Close
</button>
</div>
</Portal>
)}
</div>
);
}
}
export default Overlay;
When mapping on this user array it doesn't appear to give me access to the actual user data I need. Is it because I have an array within an array?
onCountryClick = (country, users) => {
this.openOverlay();
this.setState({ users: [users], country });
};
Is users an array? If so remove the brackets here or map through the first element. users[0].map()
I have two components that are both children of the same parent component that both render a list of places - a map that loads the places as markers on the map and then a grid of place listings with a filter menu. What I want to do is communicate the filter click from the place listing component to the map component to filter the markers. To accomplish this I have a function in the parent component called handlePlaceFilter() that I am passing into the place listing child component as a prop.
I am able to trigger a console log statement from the parent component after a filter click on the child and can pass up to the parent a filtered list of places - but I can't get it to re-render either component with the filtered list of places.
Here's the parent component containing both children and the handlePlaceFilter() function:
import React from 'react';
import Header from './Header';
import MapContainer from './MapContainer';
import _ from 'lodash';
import Places from './Places';
const Cosmic = require('cosmicjs')();
export default class PlaceIndex extends React.Component {
constructor (props) {
super(props);
this.handlePlaceFilter = this.handlePlaceFilter.bind(this);
this.state = {
destination: '',
destinations: '',
places: '',
globals: '',
}
}
async componentDidMount() {
const bucket = Cosmic.bucket({
slug: 'where-she-goes',
read_key: '',
write_key: ''
});
try {
let result = await bucket.getBucket()
this.setState (() => {
return {
destination: _.find(result.bucket.objects, { slug: this.props.match.params.slug }),
destinations: _.filter(result.bucket.objects, {type_slug: 'destinations'}),
places: _.filter(result.bucket.objects, {type_slug: 'places'}),
globals: _.filter(result.bucket.objects, {type_slug: 'globals'})
}
});
} catch (err) {
console.log(err)
}
}
handlePlaceFilter (places) {
console.log("Place filter clicked!")
console.log(places)
this.setState (() => {
return {
places: places
}
});
}
render() {
if (!this.state.places || !this.state.destination)
return <p>Loading...</p>
// compile list of destination plus children
let placeDestinations = new Array();
placeDestinations.push(this.state.destination.slug);
this.state.destination.metadata.child_destinations &&
this.state.destination.metadata.child_destinations.map(destination => {
placeDestinations.push(destination.slug)
destination.metadata.child_destinations &&
destination.metadata.child_destinations.map(child_destination => {
placeDestinations.push(child_destination.slug)
})
})
console.log("Destination List")
console.log(placeDestinations)
// filter places by destination list
let places = this.state.places.filter(function(place) {
return placeDestinations.includes(place.metadata.destination.slug);
})
console.log("Places")
console.log(places)
let destinationCenter = {
latitude: this.state.destination.metadata.latitude,
longitude: this.state.destination.metadata.longitude
}
return (
<div>
<Header
destinations={this.state.destinations}
globals={this.state.globals}
/>
<div className="places-title text-center">
<h2>All Places in {this.state.destination.title}</h2>
</div>
<MapContainer
places={places}
center={destinationCenter}
/>
<Places
places={places}
handlePlaceFilter={this.handlePlaceFilter}
/>
</div>
);
}
}
Here's the child component for the Place Listings:
import React from 'react'
import _ from 'lodash'
export default class Places extends React.Component {
constructor (props) {
super(props);
this.showHotels = this.showHotels.bind(this);
this.showAll = this.showAll.bind(this);
this.showRestaurants = this.showRestaurants.bind(this);
let places = _.flatMap(this.props.places, this.props.places.metadata);
var allplaces = new Array();
var hotels = new Array();
var restaurants = new Array();
var sights = new Array();
places &&
places.map(place => {
allplaces.push(place)
if (place.metadata.place_type == 'Hotel') {
hotels.push(place)
}
if (place.metadata.place_type == 'Restaurant') {
restaurants.push(place)
}
if (place.metadata.place_type == 'Sight') {
sights.push(place)
}
})
// Limit # of places in each array to customize for page contect
if (this.props.limit) {
(allplaces.length > 0) ? (allplaces.length = this.props.limit) : allplaces;
(hotels.length > 0) ? (hotels.length = this.props.limit) : hotels;
(restaurants.length > 0) ? (restaurants.length = this.props.limit) : restaurants;
(sights.length > 0) ? (sights.length = this.props.limit) : sights;
}
this.state = {
current: "All",
places: allplaces,
hotels: hotels,
restaurants: restaurants,
sights: sights,
allplaces: allplaces
}
}
showAll (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.allplaces);
this.setState (() => {
return {
current: "All",
places: this.state.allplaces
}
});
}
showHotels (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.hotels);
this.setState (() => {
return {
current: "Hotels",
places: this.state.hotels
}
});
}
showRestaurants (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.restaurants);
this.setState (() => {
return {
current: "Restaurants",
places: this.state.restaurants
}
});
}
showSights (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.sights);
this.setState (() => {
return {
current: "Sights",
places: this.state.sights
}
});
}
render () {
if (this.state.allplaces.length > 0) {
return (
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="blogFilter text-center text-uppercase">
<ul className="list-inline">
<li>{(this.state.current == "All") ? All : <a href="#" onClick={this.showAll}>All</a>}</li>
<li>{(this.state.hotels.length > 0) ? ((this.state.current == "Hotels") ? <a href="#" className="current" onClick={this.showHotels}>Hotels</a> : <a href="#" onClick={this.showHotels}>Hotels</a>) : <span></span>}</li>
<li>{(this.state.restaurants.length > 0) ? ((this.state.current == "Restaurants") ? <a href="#" className="current" onClick={this.showRestaurants}>Restaurants</a> : <a href="#" onClick={this.showRestaurants}>Restaurants</a>) : <span></span>}</li>
<li>{(this.state.sights.length > 0) ? ((this.state.current == "Sights") ? <a href="#" className="current" onClick={this.showSights}>Sights</a> : <a href="#" onClick={this.showSights}>Sights</a>) : <span></span>}</li>
</ul>
</div>
<div className="row">
<div className="blogContainer">
{
this.state.places &&
this.state.places.map(place => {
console.log("Places")
console.log(place)
return (
<div className="col-sm-3 design">
<article className="portfolio portfolio-2 post-grid">
<div className="post-thumb">
<a href={`/place/${place.slug}`}><img src={place.metadata.hero.imgix_url} alt="" /></a>
<div className="post-thumb-overlay text-center">
<div className="text-uppercase text-center">
<i className="fa fa-link"></i>
</i>
</div>
</div>
</div>
<div className="post-content">
<header className="entry-header text-center text-uppercase">
<h6><a href={`/place/${place.slug}`}>{place.metadata.place_type}</a></h6>
<h2 className="entry-title">{place.title}</h2>
</header>
</div>
</article>
</div>
)
})
}
</div>
</div>
</div>
</div>
</div>
)
} else {
return (
<div></div>
)
}
}
}
And here's the child component for the Map:
import React, { Component } from 'react';
import {Map, InfoWindow, Marker, GoogleApiWrapper} from 'google-maps-react';
const mapStyles = {
width: '100%',
height: '300px'
};
let geocoder;
export class MapContainer extends Component {
constructor (props) {
super(props);
this.onMarkerClick = this.onMarkerClick.bind(this);
this.displayMarkers = this.displayMarkers.bind(this);
let addresses = new Array();
this.props.places &&
this.props.places.map(place => {
addresses.push(place.metadata.address)
})
this.state = {
lat: this.props.center.latitude,
lng: this.props.center.longitude,
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {},
places: [],
addresses: addresses
}
}
componentDidMount () {
this.plotPoints()
}
plotPoints () {
let locations = this.getPoints(geocoder)
let places = new Array()
Promise.all(locations)
.then((returnVals) => {
returnVals.forEach((latLng) => {
let place = {
latitude: latLng[0],
longitude: latLng[1]
}
places.push(place)
})
console.log("Places to Plot:")
console.log(places[0].latitude)
// places now populated
this.setState(() => {
return {
lat: places[0].latitude,
lng: places[0].longitude,
places: places
}
});
console.log("Center Lat")
console.log(this.state.lat)
console.log(this.state.lng)
});
}
getPoints(geocoder) {
let locationData = [];
for (let i = 0; i < this.state.addresses.length; i++) {
locationData.push(this.findLatLang(this.state.addresses[i], geocoder))
}
return locationData // array of promises
}
findLatLang(address, geocoder) {
return new Promise(function(resolve, reject) {
geocoder.geocode({
'address': address
}, function(results, status) {
if (status === 'OK') {
console.log(results);
resolve([results[0].geometry.location.lat(), results[0].geometry.location.lng()]);
} else {
reject(new Error('Couldnt\'t find the location ' + address));
}
})
})
}
displayMarkers (stores) {
return stores.map((place, index) => {
return <Marker key={index} id={index} position={{
lat: place.latitude,
lng: place.longitude
}}
onClick={() => console.log("You clicked me!")} />
})
}
onMarkerClick (props, marker, e) {
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
});
};
render() {
geocoder = new this.props.google.maps.Geocoder();
console.log("Place Array")
console.log(this.state.places)
return (
<div className="container place-map">
<div className="row">
<div className="col-md-12">
<Map
google={this.props.google}
zoom={8}
style={mapStyles}
initialCenter={{
lat: this.state.lat,
lng: this.state.lng
}}
>
{this.displayMarkers(this.state.places)}
<InfoWindow
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}
>
<div>Your Location Here!</div>
</InfoWindow>
</Map>
</div>
</div>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: 'AIzaSyCOJDrZ_DXmHzbzSXv74mULU3aMu3rNrQc'
})(MapContainer);
In your Child component, you are retrieving/setting places value in constructor. After which any change in props of Parent component wont notify to Child component that state value is updated, unless you add getDerivedStateFromProps(props, state).
In this method, you will receive new props and update state from newly received props.
After updating state here (with setState, your child component's render method will execute)
For component to rerender and display the changes the state needs to update. Right now youbare update the state with initial props. When the props changes the state of your child component won't change as you are using only initial props. So what you can do is use a lifecycle hook componentWillReceiveProps and inside that update your state with new props.
Code can be something like this:
componentWillReceiveProps(nextProps){
if(this.state.lat !== nextProps.center.latitude){
this.setState({ lat: nexrProps.center.latitude});
}
}
You can do likewise for rest of the variables too.
This way whenever your props changes your state will also change thus forcing your component to rerender and reflect the changes.
Hope it helps!!
I'm building a leaflet map that dipslay markers. These markers have additional data on them besides lat and long, (keys in this case)
I'm using a custom react component from the react-leaflet repo. I can get multiple markers location, but when I try to retrieve the marker I clicked on and pass it to a child component I get
Cannot read property '# < Object > ' of undefined
I can pass ALL the data , without a problem though.
I get this error when I try to pass the "clicked" marker to the Card and Cartitle components.
What I've tried so far:
Inside the handleToggle function I did:
index = this.props.places[index]
but that is when I get the error above. What am I doing wrong?
P.D. Also, Im able to display ALL the data to the children components
import ...
const markers = [
{
key: 'P12345678',
position: [37.786464, -122.411047],
children: 'My first popup'
},
{
key: 'M12345678',
position: [40.689192, -74.044563],
children: 'My second popup'
},
];
class Mapper extends Component {
constructor(props) {
super(props);
this.handleToggle = this.handleToggle.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
lat: 29.761993,
lng: -95.366302,
zoom: 4,
open: false,
places: []
}
}
handleToggle(index) {
const self = this;
self.setState({
open: !this.state.open,
places: markers
});
// index = this.props.places[index]
console.log(this.state.places)
}
handleClose() {
this.setState({
open: false
});
}
render() {
const center = [this.state.lat, this.state.lng];
//Custom Marker Component
const MyPopupMarker = ({ children, position }) => (
<Marker
onClick={this.handleToggle}
position={position}
places={this.state.places}
>
<Popup>
<span>{children}</span>
</Popup>
</Marker>
)
MyPopupMarker.propTypes = {
// children: MapPropTypes.func,
// position: MapPropTypes.latlng,
}
//Custom Marker List Component
const MarkerList = ({ markers }) => {
const items = markers.map(({ key, ...props }) => (
<MyPopupMarker key={key} {...props} />
))
return <div style={{display: 'none'}}>{items}</div>
}
MarkerList.propTypes = {
markers: MapPropTypes.array.isRequired,
}
// console.log('markers', markers)
return (
<div>
<Map
center={center}
zoom={this.state.zoom}
style={styles.map}>
<TileLayer
url='https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png'
attribution='© OpenStreetMap contributors'
/>
<MarkerList markers={markers} />
</Map>
<Drawer
width={500}
openSecondary={true}
docked={false}
open={this.state.open}
onRequestChange={(open) => this.setState({open})}
containerStyle={styles.whitebox}
>
{
markers.map((cf, k) => (
<Card
style={styles.appMedia}
key={k}
>
<CardTitle
titleStyle={styles.drawTitle}
subtitleStyle={styles.drawTitle}
title={cf.key}
subtitle="Common Field Equipment"/>
</Card>
))
}
</Drawer>
</div>
);
}
}
export default Mapper;
Your handleToggle method is the onClick handler for the Marker component. The onClick handlers in React pass a synthetic event object by default (More info on this here).
So the index passed into the handleToggle method will be that synthetic event object, which would not help you get the index of the marker from your places array. You would have to pass the index explicitly into your handleToggle method. To do this you would need to make the following changes:
1> Change the binding of the function in the constructor to:
this.handleToggle = this.handleToggle.bind(this,index);
2> Pass the index explicitly in the MyPopupMarker component:
const MyPopupMarker = ({ children, position,index }) => (
<Marker
onClick={()=>this.handleToggle(index)}
....
)
3> Pass the index to the MyPopupMarker component in the MarkerList component
const items = markers.map(({ key, ...props },i) => (
<MyPopupMarker key={key} index={i} {...props}/>
))
4> Access the index from the places array in the state object and not the props object.
handleToggle(index) {
const self = this;
self.setState({
open: !this.state.open,
places: markers
});
var selectedMarker = this.state.places[index];
console.log(this.state.places)
}
The reason for the error message Cannot read property '# < Object > ' of undefined was that you were trying to access the places array in the props object where it was undefined.
As an aside, I would like to point out that creating components inside the render method (the components MyPopupMarker and MarkerList) is not a good practice as these components would be created on every render which would be wasteful. It would be much more efficient if you declare them outside your Mapper component.
Edit:
To move the components out of render you need to pass the handleToggle function in as props. I have changed your code as follows:
The MyPopupMarker component: This component gets the index and the handleToggle function as props.
const MyPopupMarker = ({ children, position, index, handleToggle }) => (
<Marker onClick={() => handleToggle(index)} position={position}>
<Popup>
<span>{children}</span>
</Popup>
</Marker>
);
The MarkerList component: This component gets the handleToggle function as props and passes it on to the MyPopupMarker component along with the index
const MarkerList = ({ markers, handleToggle }) => {
const items = markers.map(({ key, ...props }, i) => (
<MyPopupMarker key={key} {...props} index={i} handleToggle={handleToggle} />
));
return <div >{items}</div>;
};
The Mapper component: This component passes the handleToggle function to the MarkerList component along with the markers
class Mapper extends Component {
constructor(props) {
super(props);
this.handleToggle = this.handleToggle.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
lat: 29.761993,
lng: -95.366302,
zoom: 4,
open: false,
places: []
};
}
handleToggle(index) {
const self = this;
self.setState({
open: !this.state.open,
places: markers
});
// index = this.props.places[index]
let selectedMarker = this.state.places[index];
console.log(selectedMarker);
}
handleClose() {
this.setState({
open: false
});
}
render() {
const center = [this.state.lat, this.state.lng];
let selectedMarker = this.state.places;
return (
<div>
<Map center={center} zoom={this.state.zoom} style={styles.map}>
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
/>
<MarkerList markers={markers} handleToggle={this.handleToggle} />
</Map>
<Drawer
width={500}
openSecondary={true}
docked={false}
open={this.state.open}
onRequestChange={open => this.setState({ open })}
containerStyle={styles.whitebox}
>
{selectedMarker.map((value, index) => (
<Card style={styles.appMedia} key={index}>
<CardTitle
titleStyle={styles.drawTitle}
subtitleStyle={styles.drawTitle}
title={value.key}
subtitle="Common Field Equipment"
/>
</Card>
))}
</Drawer>
</div>
);
}
}
export default Mapper;
The full working example can be found at https://codesandbox.io/s/4r1yo07kw9