I have 3 components: App, Map and ListPlaces. In ListPlaces component, when a user types something in the input element, I want to change the state(markers's state) in App.js to show only related markers on the map.
Edit: When I edit my typo, the error was disappeared. However, I think the logic is still wrong. Because when I write something in the input element, markers array would be 0 immediately. And of course, all markers are disappeared.
More Explanation:
After componentDidMount, my markers array holds 7 items. And Map component takes this markers array and render markers on the map. However, I need to control my markers from ListPlaces component according to value of input element. So I put this: onChange={e => {this.updateQuery(e.target.value); changeMarkersHandler(e.target.value)}} in onChange attribute of input element. (Omit the this.updateQuery, for now, you can focus on only changeMarkersHandler).
This changeMarkersHandler runs changeMarkers function in App.js, but I don't know why my marker arrays would be 0 immediately while changeMarkers function is working.
Note: I am using react-google-maps and I've omitted some code blocks which aren't related to question.
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);
});
}
changeMarkers(value) {
const newMarkers = this.state.markers.filter(
place => place.name === value
);
this.setState({
newMarkers : newMarkers,
markers: newMarkers
})
}
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="url_here" />
<ListPlaces changeMarkersHandler={this.changeMarkers} />
</div>
);
}
}
ListPlaces.js
import React, { Component } from "react";
import escapeRegExp from "escape-string-regexp";
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>some text</p>
</aside>
<a
onClick={toggleListHandler}
id="nav-toggle"
className="position"
>
<span />
</a>
</div>
);
}
}
export default ListPlaces;
You have a typo in you constructor.
this.changeMarkers(this.changeMarkers.bind(this));
should be
this.changeMarkers = this.changeMarkers.bind(this);
Related
I am trying to implement an onChange method that when the user type something it gets updated in real time and displayed in the div. The component that I am talking about is at the end of the code and it's called and it is an input that will be rendered 4 times on the dom. For a reason no value get shown on the div I mean {this.state.stake}. Could anyone help me in fixing that? Thanks
import React, { Component } from 'react';
import Stake from './stake';
class FetchRandomBet extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
bet: null,
value: this.props.value,
stake: ''
};
}
async componentDidMount() {
const url = "http://localhost:4000/";
const response = await fetch(url);
const data = await response.json();
this.setState({
loading: false,
bet: data.bets,
});
}
changeStake = (e) => {
this.setState({
stake: [e.target.value]
})
}
render() {
const { valueProp: value } = this.props;
const { bet, loading } = this.state;
if (loading) {
return <div>loading..</div>;
}
if (!bet) {
return <div>did not get data</div>;
}
return (
< div >
{
loading || !bet ? (
<div>loading..</div>
) : value === 0 ? (
<div className="bet-list">
<ol>
<p>NAME</p>
{
bet.map(post => (
<li key={post.id}>
{post.name}
</li>
))
}
</ol>
<ul>
<p>ODDS</p>
{
bet.map(post => (
<li key={post.id}>
{post.odds[4].oddsDecimal}
<div className="stake-margin">
<Stake
onChange={this.changeStake} />
{this.state.stake}
</div>
</li>
))
}
</ul>
</div>
Pass this.state.stake as a prop of Stake component.
<Stake
onChange={this.changeStake}
stake={this.state.stake}
/>
Then inside of the Stake component assign stake prop to value on an the input. It would look something like this.
const Stake =({stake, onChange})=>{
return <input value={stake} onChange={onChange} />
}
My code generates an input field that allows a user to enter a value to search for. Then when they click the Submit button, it causes displayMap to be true, so that when the MapDisplay component renders, it will trigger an API search via the Map component and return values that are then displayed on the map.
The problem is that this process only works once. When I click the button again, it does do something, I confirmed that it is getting the new value in the input box, but I can't seem to figure out how to get the map to be rendered again.
I've tried setting other variables in the this.setState to try to get it to know that it needs to render the component again, but I guess I'm missing something, because nothing works.
I'm fairly new to React, so any help you can offer would be greatly appreciated.
This is the MainSearchBar.js, where most of the work as described above is happening:
import Map from './newMap.js';
function MapDisplay(props) {
if (props.displayMap) {
return <Map toSearch = {props.searchTerm}></Map>;
} else {
return "";
}
}
class MainSearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
displayMap: false,
value: '',
searchTerm: '',
isOpened: false
};
//this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleClick = () => {
this.setState({
displayMap: true,
isOpened: !this.state.isOpened,
searchTerm: this.state.value
});
console.log(this.state.value);
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
const displayMap = this.state.displayMap;
return (
<div class="homepage-search-bar">
<input
type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
</input>
<button onClick={this.handleClick}>Search</button>
<MapDisplay displayMap={displayMap} searchTerm={this.state.value} />
</div>
)
}
}
export default MainSearchBar;
This is where MainSearchBar is being called from
import Top20Box from '../components/getTop20Comp2.js';
import Header from '../components/Header.js';
import MainIntro from '../components/MainIntro.js';
import MainSearchBar from '../components/MainSearchBar.js';
import MainCTA from '../components/MainCTA.js';
import Footer from '../components/Footer.js';
export default class Home extends Component {
state = {
}
render () {
return (
<React.Fragment>
<Header>
</Header>
<MainIntro />
<MainSearchBar />
<div className="top20-text">
Top 20 trending hashtags
</div>
<Top20Box />
<MainCTA />
<Footer />
</React.Fragment>
)
}
}
And this is the Map component itself, in case you need it:
import React from 'react';
import ReactMapGL, {Marker, Popup} from 'react-map-gl';
import axios from 'axios';
//for the loading animation function
import FadeIn from "react-fade-in";
import Lottie from "react-lottie";
import * as loadingData from "../assets/loading.json";
var locationCoordinates = [];
var locationToSearch = "";
var returnedKeywordSearch = [];
var newArray = [];
const defaultOptions = {
loop: true,
autoplay: true,
animationData: loadingData.default,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice"
}
};
export default class Map extends React.Component {
//sets components for the map, how big the box is and where the map is centered when it opens
state = {
viewport: {
width: "75vw",
height: "50vh",
latitude: 40.4168,
longitude: 3.7038,
zoom: .5
},
tweetSpots: null, //data from the API
selectedSpot: null,
done: undefined, //for loading function
};
async componentDidMount() {
//searches the api for the hashtag that the user entered
await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
returnedKeywordSearch = response.data;
}) //if the api call returns an error, ignore it
.catch(function(err) {
return null;
});
//goes through the list of locations sent from the api above and finds the latitude/longitude for each
var count = 0;
while (count < returnedKeywordSearch.length) {
locationToSearch = returnedKeywordSearch[count].location;
if (locationToSearch !== undefined) {
var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
.catch(function(err) {
return null;
});
if (locationList !== null) {
if (Array.isArray(locationList.data.features) && locationList.data.features.length)
{
locationCoordinates.push(locationList.data.features[0].center);
if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
&& locationList.data.features[0].center !== undefined)
{newArray.push({
id: returnedKeywordSearch[count].id,
createdAt: returnedKeywordSearch[count].createdAt,
text: returnedKeywordSearch[count].text,
name: returnedKeywordSearch[count].name,
location: returnedKeywordSearch[count].location,
coordinates: locationList.data.features[0].center
});
}
}
}
}
count++;
}
this.setState({tweetSpots: newArray});
this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
}
//is triggered when a marker on the map is hovered over
setSelectedSpot = object => {
this.setState({
selectedSpot: object
});
};
//creates markers that display on the map, using location latitude and longitude
loadMarkers = () => {
return this.state.tweetSpots.map((item,index) => {
return (
<Marker
key={index}
latitude={item.coordinates[1]}
longitude={item.coordinates[0]}
>
<img class="mapMarker"
onMouseOver={() => {
this.setSelectedSpot(item);
}}
src="/images/yellow7_dot.png" alt="" />
</Marker>
);
});
};
//closes popup when close is clicked
closePopup = () => {
this.setState({
selectedSpot: null
});
};
//renders map component and loading animation
render() {
return (
<div className="App">
<div className="map">
{!this.state.done ? (
<FadeIn>
<div class="d-flex justify-content-center align-items-center">
<Lottie options={defaultOptions} width={400} />
</div>
</FadeIn>
) : (
<ReactMapGL {...this.state.viewport} mapStyle="mapbox://styles/mapbox/outdoors-v11"
onViewportChange={(viewport => this.setState({viewport}))}
mapboxApiAccessToken="pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg">
{this.loadMarkers()}
{this.state.selectedSpot !== null ? (
<Popup
key={this.state.selectedSpot.id}
tipSize={5}
latitude={this.state.selectedSpot.coordinates[1]}
longitude={this.state.selectedSpot.coordinates[0]}
closeButton={true}
closeOnClick={false}
onClose={this.closePopup}
>
<div className="mapPopup">
<div className="header"> Tweet </div>
<div className="content">
{" "}
<p>
<b>Name:</b> {this.state.selectedSpot.name}
</p>
<p>
<b>Tweet:</b> {this.state.selectedSpot.text}</p>
<p>View Tweet in Twitter
</p>
</div>
</div>
</Popup>
) : null}
</ReactMapGL>
)}
</div>
</div>
);
}
}
Update: 4/28, per the answer I received, I update the render of the MainSearchBar.js to look like this:
render() {
const displayMap = this.state.displayMap;
return (
<div class="homepage-search-bar">
<input
type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
</input>
<button onClick={this.handleClick}>Search</button>
{this.state.displayMap && <Map toSearch = {this.searchTerm}></Map>}
</div>
)
}
When you click the button again, the state of MainSearchBar.js updates but the functional component MapDisplay does not and thus the Map does not update as well.
There are many ways to resolve this. Looking at the code, it looks like MapDisplay doesn't do much so you can consider replacing it with conditional rendering.
MainSearchBar.js
render() {
const displayMap = this.state.displayMap;
return (
<div class="homepage-search-bar">
<input
type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
</input>
<button onClick={this.handleClick}>Search</button>
{this.state.displayMap && <Map toSearch = {props.searchTerm}></Map>}
</div>
)
}
Then in your Map component, add a componentDidUpdate lifecycle method to detect updates to the prop which does the same thing as componentDidMount when the props are updated.
async componentDidMount(prevProps) {
if (props.toSearch != prevProps.toSearch) {
await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
returnedKeywordSearch = response.data;
}) //if the api call returns an error, ignore it
.catch(function(err) {
return null;
});
//goes through the list of locations sent from the api above and finds the latitude/longitude for each
var count = 0;
while (count < returnedKeywordSearch.length) {
locationToSearch = returnedKeywordSearch[count].location;
if (locationToSearch !== undefined) {
var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
.catch(function(err) {
return null;
});
if (locationList !== null) {
if (Array.isArray(locationList.data.features) && locationList.data.features.length)
{
locationCoordinates.push(locationList.data.features[0].center);
if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
&& locationList.data.features[0].center !== undefined)
{newArray.push({
id: returnedKeywordSearch[count].id,
createdAt: returnedKeywordSearch[count].createdAt,
text: returnedKeywordSearch[count].text,
name: returnedKeywordSearch[count].name,
location: returnedKeywordSearch[count].location,
coordinates: locationList.data.features[0].center
});
}
}
}
}
count++;
}
this.setState({tweetSpots: newArray});
this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
}
}
#wxker Thanks for all your help! You certainly got me pointed in the right direction.
I changed render in MainSearchBar.js back to what it was originally.
And I added a ComponentDidUpdate to the Map component, as follows below:
async componentDidUpdate(prevProps) {
//searches the api for the hashtag that the user entered
if (this.props.toSearch !== prevProps.toSearch) {
and then the rest was the same as the original componentDidMount.
In my website, it currently shows users a list of movies based on their input.
When user clicks on the title of the movie that is rendered, I want to setState(chosenOne: the movie title they clicked).
Currently, when I click on movie title, it returns an error stating the following:
Uncaught TypeError: Cannot read property 'onClick' of undefined
at onClick (Fav.js:62)
Any way to fix this?
Any help is greatly appreciated.
import React, { Component } from 'react'
import axios from 'axios';
import '../styles/Rec.scss'
export class Fav extends Component {
constructor (props) {
super (props)
this.state = {
inputOne: '',
chosenOne: '',
movies:[],
};
}
onChangeOne = (event) => {
this.setState({
inputOne: event.target.value
},()=>{
if(this.state.inputOne && this.state.inputOne.length > 1) {
this.getInfo()
} else {
}
})
}
onClick = (event) =>{
this.setState({
chosenOne: event.currentTarget.textContent
})
console.log(this.state.chosenOne)
}
onSubmit = (event) => {
event.preventDefault();
}
getInfo = () => {
let url = `https://api.themoviedb.org/3/search/movie?api_key=''&language=en-US&query='${this.state.inputOne}'&page=1&include_adult=false`
axios.get(url)
.then(res => {
if (res.data) {
const movieData = res.data.results.filter(movie => movie.poster_path != null);
this.setState({movies: movieData});
}
console.log(this.state.movies)
})
}
render() {
return (
<div>
<h1>Favorite Movie of All Time</h1>
<form onSubmit={this.onSubmit}>
<input onChange={this.onChangeOne}/>
<div className="rec__container">
{this.state.movies && this.state.movies.slice(0,3).map(function(movie, genre_ids) {
return(
<div className="rec__sample">
<img className="rec__img" src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} alt="movie poster"/>
<p onClick={event => this.onClick(event)}>{movie.title}</p>
</div>
)
})}
</div>
</form>
</div>
)
}
}
export default Fav
I am quite sure the problem is that this in this.onClick is undefined. This happens when it is not bound correctly to the the class.
I would recommend to change the function declared after map to an arrow function.
<div className="rec__container">
{this.state.movies &&
this.state.movies.slice(0, 3).map((movie, genre_ids) => {
return (
<div className="rec__sample">
<img className="rec__img" src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} alt="movie poster" />
<p onClick={event => this.onClick(event)}>{movie.title}</p>
</div>
);
})}
</div>;
Also, you are binding onClick function in your constructor as well as using it as an arrow function. Bind does not work on arrow functions.
Either remove this.onClick = this.onClick.bind(this) or convert onClick into a simple function rather than an arrow function.
I am working on a quiz in react. I want to show one question and their choices at the same time at the page.
Such as :
"question": "When the C programming language has first appeared?",
a.)1970
b.)1971
c.)1972
d.)1973
This is what I have done so far:
import React from 'react'
import axios from 'axios'
class QuizApp extends React.Component {
constructor(props) {
super(props);
this.state = {entered: false, correct: 0, wrong: 0}
}
state = {
questions: [],
choic: []
};
componentDidMount() {
axios.get("http://private-anon-c06008d89c-quizmasters.apiary-mock.com/questions").then((response) => {
const questions = response.data;
this.setState({questions});
const choic = response.data;
this.setState({choic});
console.log(response);
})
}
nickChange = (event) => {
this.setState({username: event.target.value});
};
cleanPage = () => {
this.setState({
entered: true
})
};
render() {
if (!this.state.entered) {
return (
<div>
<form target="_self" id="firstPage">
<input type="text" value={this.nick} onChange={this.nickChange}/>
<input type="submit" value="Start" name="cleanPage" onClick={this.cleanPage}/>
</form>
</div>
)
}
else {
return (
<div>
<ul>
{this.state.questions.map(que => <li>{que.question} </li>)}
{this.state.choic.map(cho => <li>{cho.choices.choice} </li>)}
</ul>
</div>
)
}
}
}
simplify set state in response
const { questions, choices } = response.data;
this.setState({questions, choices});
In render do like this (Assuming each question has corresponding array of choices in that index):
{this.state.questions.map((que, index) => {
<React.Fragment>
<li>{que.question} </li>
<ul>
{choices[index].map(choice => <li>{choice}</li>)
</ul>
<React.Fragment
})}
style the elements as needed
After making post request the data post to the database and comes back in the response but props doesn't get passed down in time causing a undefined error in props resulting in blank screen. The ApiManager is a generic crud controller to handle request. I want to know should I use componentWillReceiveProps componentDidUpdate or shouldComponentUpdate and what are generally the best practices for child components updating appropriately
import React, { Component } from 'react'
import ZoneData from './ZoneData'
import { ApiManager } from '../utils'
class Zones extends Component {
constructor(){
super()
this.state = {
zone:{
name:'',
zipcode: ''
},
list: []
}
}
componentDidMount(){
ApiManager.get('http://localhost:3033/api/zone', null, (err, response) => {
if(err){
console.log(err)
return
}
console.log(response)
this.setState({
list: response.results
})
})
}
updateZone (event) {
event.preventDefault()
console.log('updateZone: '+event.target.id+'=='+event.target.value)
let updateZone = Object.assign({}, this.state.zone)
updateZone[event.target.id] = event.target.value
this.setState({
zone: updateZone
})
}
addZone () {
let updatedZone = Object.assign({}, this.state.zone)
ApiManager.post('http://localhost:3033/api/zone', updatedZone, (err, response) => {
if(err){
alert('ERROR '+err.message)
return
}
console.log("Post created: "+JSON.stringify(response))
let zoneList = Object.assign([], this.state.list)
zoneList.push(response.result)
this.setState({
list: zoneList
})
})
}
render(){
const listItems = this.state.list.map((zone, i) => {
return <li className="list-group-item" key={i}><ZoneData currentZone={zone}/></li>
})
return (
<div className="position-sticky">
<ul className="list-group">
{listItems}
</ul>
<input id='name' onChange={this.updateZone.bind(this)} className='form-control' placeholder="name"/>
<input id='zipcode' onChange={this.updateZone.bind(this)} className='form-control' placeholder="zipcode"/>
<button onClick={this.addZone.bind(this)} className='btn btn-primary'>Add Zone</button>
</div>
)
}
}
export default Zones
child component
import React, { Component } from 'react'
const ZoneData = (props) => {
return (
<div className="container">
<h2>{props.currentZone.name}</h2>
<span>{props.currentZone.zipcode}</span><br/>
<span>{props.currentZone.numPosts}</span>
</div>
)
}
export default ZoneData
child component props.currentZone.name comes back as undefined after making post request and the screen turns blank with out updating the child components props.
when I refresh the the new data that was posted to the database is there
I think, you should add some variable like isListReady to check if list is ready for showing and everything will be ok.
render(){
const {list} = this.state;
const isListReady = list && list.length;
const listItems = isListReady && list.map((zone, i) => {
return <li className="list-group-item" key={i}><ZoneData currentZone={zone}/></li>
})
return (
<div className="position-sticky">
<ul className="list-group">
{isListReady && listItems}
</ul>
<input id='name' onChange={this.updateZone.bind(this)} className='form-control' placeholder="name"/>
<input id='zipcode' onChange={this.updateZone.bind(this)} className='form-control' placeholder="zipcode"/>
<button onClick={this.addZone.bind(this)} className='btn btn-primary'>Add Zone</button>
</div>
)
}
You are getting the blank screen because you should use componentWillMount() instead of componentDidMount(). componentDidMount() makes an api call after the component has rendered. And I think you also need to do conditional rendering too, for listItems. Something like this:
render(){
const listItems = this.state.list.map((zone, i) => {
return <li className="list-group-item" key={i}><ZoneData currentZone={zone}/></li>
})
return (
<div className="position-sticky">
<ul className="list-group">
{this.state.isListAvailable && listItems}
</ul>
{// other code}
</div>
)
}
you can set isListAvailable to true when you have the list and the data.
Maybe its vacation time but it was a typo on response.result should have been response.results