I am fetching data using the Foursquare API within a React app using the Google Map API and react-google-map.
When I create a list of venues and try to check if the error handling works by deleting some characters from the API link, the app crashes because the venues state is empty.
I should be adding a conditional statement to the function that maps through the data to check if it's null, but I don't know how to do it.
Here is my repository:
https://github.com/Katesold/Watford-Map
If someone could advise me on how to do it, I would really appreciate it.
What I have been trying looks like this:
filterCafes = (cafes, query) => {
cafes !== undefined ? cafes.filter(cafe =>
cafe.name.toLowerCase().includes(query.toLowerCase())): "error";
}
Here's the error I got:
app crashes
Welcome to StackOverflow.
Your problems can be alleviated by ensuring that cafes is set to null by default. Then in Sidebar1.js, I have added a truthy check to your filter that you used in the render method (cafePlaces && cafePlaces.filter...). This means that the filter will only be executed if cafePlaces was successfully fetched in your parent component, where you fetch the data.
You will also need to update Places.js, as you use cafes in there too. I have also added an early return to the filterCafes method as it will error out when you try filtering as not cafes were loaded into the parent state if the API called failed.
(P.S. Please note I broke your API call on purpose in the example code provided below).
App.js
import React, { Component } from "react";
import Map from "./Map";
import SideBar from "./SideBar1.js";
import "./App.css";
import Header from "./Header.js";
import Footer from "./Footer.js";
class App extends Component {
state = {
cafes: null,
clickedCafe: {},
filteredCafe: []
};
// get the data for the cafes in Watford and catch any errors from Foursquare and Map
componentDidMount() {
fetch(
"https://api.foursquare.com/v2/venues/search?ll=51.656489,-.39032&intent=browse&radius=10000&client_id=XQSXUGIR140AWUVFJJ120S31IPIXQYIO2QJ2ZN2U0ZPLLG4P&client_secret=A0N5P5VI4NG5UQK2GV2M0WU1FYY3KZ0EUYV0YMYZSX5IHHSU&v=26"
)
.then(response => response.json())
.then(data => {
this.setState({
cafes: data.response.venues,
filteredCafe: data.response.venues
});
})
.catch(error => {
alert(
"An error occurred while trying to fetch data from Foursquare: " +
error
);
});
window.gm_authFailure = () => {
alert("An error occurred while trying to load Google Map");
};
}
// Update filtered list of venues
updateList = filteredCafe => {
this.setState({ filteredCafe });
};
// Show the infowindow when a place is clicked
handleInfoWindow = clickedCafe => {
this.setState({ clickedPlace: clickedCafe });
this.setState({ menuHidden: false });
};
render() {
return (
<div className="app" role="application" aria-label="map">
<Header />
<Map
cafes={this.state.filteredCafe}
clickedPlace={this.state.clickedPlace}
handleInfoWindow={this.handleInfoWindow}
/>
<SideBar
cafes={this.state.cafes}
handleInfoWindow={this.handleInfoWindow}
updateList={this.updateList}
menuHidden={this.state.menuHidden}
/>
<Footer />
</div>
);
}
}
export default App;
Sidebar1.js
import React, { Component } from "react";
class SideBar extends Component {
state = {
query: ""
};
//filter the cafes depending on the search
refreshQuery = query => {
this.setState({ query });
this.props.updateList(this.filterCafes(this.props.cafes, query));
};
filterCafes = (cafes, query) => {
if (!cafes) {
return;
}
cafes.filter(cafe => cafe.name.toLowerCase().includes(query.toLowerCase()));
};
//cafes displayed in SideBar
render() {
const cafePlaces = this.props.cafes;
const typedQuery = this.state.query;
const listCafes =
cafePlaces &&
this.filterCafes(cafePlaces, typedQuery).map((cafe, idx) => {
return (
<li
key={cafe.id}
className="cafe"
tabIndex={0}
aria-label={cafe.name}
onClick={() => {
this.props.handleInfoWindow(idx);
}}
onKeyPress={() => {
this.props.handleInfoWindow(idx);
}}
>
{cafe.name}
</li>
);
});
return (
<aside>
<div className="sideBar">
<div className="locations-list">
<input
type="text"
placeholder="Search for a place"
aria-label="Type to look for a cafe"
value={this.state.query}
onChange={e => this.refreshQuery(e.target.value)}
/>
<ul aria-labelledby="Cafe list">{listCafes}</ul>
</div>
</div>
</aside>
);
}
}
export default SideBar;
Places.js
import React, { Component } from "react";
import {
withScriptjs,
withGoogleMap,
GoogleMap,
Marker,
InfoWindow
} from "react-google-maps";
class Map extends Component {
render() {
const places = this.props.cafes;
const animatePlace = this.props.clickedPlace;
/* create Google Map App and markers, infowindow from FourSquare API from https://github.com/tomchentw/react-google-maps/blob/master/src/docs/configuration.md
and https://tomchentw.github.io/react-google-maps/#infowindow and https://github.com/tomchentw/react-google-maps/issues/753 */
const style = {
height: "100%"
};
const styleMap = {
height: "600px",
width: "100%"
};
//define map with markers and infowindow then return it below to display within container div
const WMap = withScriptjs(
withGoogleMap(props => (
<GoogleMap
defaultZoom={14}
defaultCenter={{ lat: 51.656489, lng: -0.39032 }}
>
{places &&
places.map((place, i) => (
<Marker
key={i}
position={{ lat: place.location.lat, lng: place.location.lng }}
id={place.id}
name={place.name}
onClick={() => {
this.props.handleInfoWindow(i);
}}
animation={
animatePlace === i ? window.google.maps.Animation.DROP : null
}
>
{animatePlace === i && (
<InfoWindow onCloseClick={props.onToggleOpen}>
<div
className="infoWindow"
tabIndex={0}
aria-label="Infowindow"
>
<h2>{place.name}</h2>
<hr />
<p>
<strong>Address: </strong>
{place.location.formattedAddress[0]}
</p>
<p>{place.location.formattedAddress[1]}</p>
<p>{place.location.formattedAddress[2]}</p>
<p>{place.location.formattedAddress[3]}</p>
<p>{place.location.formattedAddress[4]}</p>
</div>
</InfoWindow>
)}
</Marker>
))}
</GoogleMap>
))
);
return (
<div className="map">
<div className="wmap" role="application">
<WMap
googleMapURL="https://maps.googleapis.com/maps/api/js?key=AIzaSyCFk8F7SikfJihxgfeWargVEIsb31hwlwA&v=3.exp"
loadingElement={<div style={style} />}
containerElement={<div style={styleMap} />}
mapElement={<div style={style} />}
/>
</div>
</div>
);
}
}
export default Map;
Related
I have the stake component that is rendered 4 times in the parent class component. I am trying to pass valueNewStake as prop to its parent component and group all the inputs in one common array (see allStakes). For a reason I am not able to change the state and also the dom does not render the button next to the component. Can anyone explain me why it is happening as I am new in react. 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,
allStakes: ['']
};
}
async componentDidMount() {
const url = "http://localhost:4000/";
const response = await fetch(url);
const data = await response.json();
this.setState({
loading: false,
bet: data.bets,
});
}
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
allStakes={this.props.valueNewStake}
onChange={() => { this.setState({ allStakes: [...this.props.valueNewStake] }) }}
>
<button>ok</button>
</Stake>
</div>
</li>
))
}
</ul>
</div>
import React, { useState } from 'react';
import CurrencyInput from 'react-currency-input-field';
function Stake() {
const [newStake, setStake] = useState(['']);
const changeStake = (e) => {
setStake(e.target.value)
}
return (
<>
<CurrencyInput
onChange={changeStake}
valueNewStake={newStake}
style={{
marginLeft: "40px",
width: "50px"
}}
placeholder="Stake"
decimalScale={2}
prefix="£"
/>
{newStake}
</>
);
}
export default Stake;
You're not passing your props to your Stake component
function Stake({ allStakes, onChange }) {
// do something with your props here
const [newStake, setStake] = useState(['']);
const changeStake = (e) => {
onChange()
setStake(e.target.value)
}
return (
<>
<CurrencyInput
onChange={changeStake}
valueNewStake={newStake}
style={{
marginLeft: "40px",
width: "50px"
}}
placeholder="Stake"
decimalScale={2}
prefix="£"
/>
{newStake}
</>
);
}
I have a problem with my react app. I have a blog page where I can create blog posts and display them to the screen. In this part everything works fine. User logs in and can write a post. Each post contains a Read more... link and if the user clicks on that link the app redirects to the actual blog post. There the user can read the whole blog and add some comments. Everything works perfectly except when the user refreshes the page, everything disappears without any error in the console. I use firebase as my back-end and everything is saved there just like it has to be. Each time I click on the particular post I get redirected to that post and everything is ok, but when I refresh the page everything disappears, the post, the comments, even the input field and the submit comment button.
Here is a picture before refresh:
Before
here is a picture after refresh:
After
Also I will include the code for the actual blog and comment section.
The BlogAndCommentPage contains the actual blog post and holds the input field for the comments and the comments that belong to this post.
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import BackToBlogs from './BackToBlogs'
import AddComment from '../commentComponents/AddComment'
class BlogAndCommentPage extends React.Component {
state = { param: '', blog: [] }
componentDidMount = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
this.setState({ param: id })
const fetchDataFromFireBase = async () => {
projectFirestore.collection('Blogs').doc(id).get()
.then(doc => {
if(doc.exists) {
let document = [];
document.push(doc.data());
this.setState({ blog: document })
}
})
}
fetchDataFromFireBase()
}
renderContent() {
// Display the blog
const blogs = this.state.blog?.map(value => {
return (
<div key={value.post.blogID}>
<h1>{value.post.title}</h1>
<h6>{`${value.post.name} - ${value.post.date}`}</h6>
<p>{value.post.body}</p>
</div>
)
})
return blogs;
}
render() {
const displayedBlog = this.state.param
return (
<div>
{
displayedBlog ? (
<div>
{this.renderContent()}
<BackToBlogs />
<hr></hr>
<h5 className="mb-2">Add a comment</h5>
<AddComment param={this.state.param} />
</div>
) : ''
}
</div>
)
}
}
export default BlogAndCommentPage
The AddComment component holds the submit button for the comments and the list of the components
import React, { useState, useEffect } from 'react'
import SubmitComment from './SubmitComment'
import CommentHolder from './CommentHolder';
import { useSelector, useDispatch } from 'react-redux';
const AddComment = ({ param }) => {
const [comment, setComment] = useState('');
useEffect(() => {
if (sessionStorage.getItem('user') === null) {
alert('You are not logged in. Click OK to log in.')
window.location = 'http://localhost:3000/'
}
}, [])
const dispatch = useDispatch();
const state = useSelector((state) => state.state);
if (state) {
setTimeout(() => {
setComment('')
dispatch({ type: "SET_FALSE" })
}, 50)
}
return (
<div>
<div>
<div className="row">
<div className="col-sm">
<div className="form-group">
<textarea rows="4" cols="50" placeholder="Comment" className="form-control mb-3" value={comment} onChange={(e) => setComment(e.target.value)} />
</div>
</div>
</div>
</div>
<div className="mb-3">
<SubmitComment comment={comment} param={param} />
</div>
<CommentHolder param={param} />
</div>
)
}
export default AddComment
The CommentHolder renders each comment that belong to that post
import React from 'react';
import { projectFirestore } from '../../firebase/config';
import DeleteComment from './DeleteComment'
class CommentHolder extends React.Component {
state = { docs: [] }
_isMounted = false;
componentDidMount = () => {
const fetchDataFromFireBase = async () => {
const getData = await projectFirestore.collection("Comments")
getData.onSnapshot((querySnapshot) => {
var documents = [];
querySnapshot.forEach((doc) => {
documents.push({ ...doc.data(), id: doc.id });
});
if (this._isMounted) {
this.setState({ docs: documents })
}
});
}
fetchDataFromFireBase()
this._isMounted = true;
}
componentWillUnmount = () => {
this._isMounted = false;
}
renderContent() {
// Delete comments
const deleteComment = async (id) => {
projectFirestore.collection('Comments').doc(String(id)).delete().then(() => {
console.log(`Blog with id: ${id} has been successfully deleted!`)
})
}
// Build comments
let user;
if (sessionStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(sessionStorage.getItem('user'));
const commentArray = this.state.docs?.filter(value => value.blogID === this.props.param)
.sort((a, b) => (a.time > b.time) ? -1 : (b.time > a.time) ? 1 : 0)
.map(comment => {
return (
<div key={comment.id} className="card mb-3" >
<div className="card-body">
<div className="row">
<div className="col-sm">
<h6>{`${comment.name} - ${comment.time}`}</h6>
<p>{comment.comment}</p>
</div>
<div className="col-sm text-right">
{user[0].id === comment.userID ? <DeleteComment commentid={comment.id} onDeleteComment={deleteComment} /> : ''}
</div>
</div>
</div>
</div>
)
});
const updateComments = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
const updateComment = projectFirestore.collection('Blogs').doc(id);
return updateComment.update({
'post.comments': commentArray.length
})
}
updateComments()
return commentArray;
}
}
render() {
return (
<div>
{this.renderContent()}
</div>
)
}
}
export default CommentHolder
The DeleteComment deletes the comment
import React from 'react'
const DeleteComment = ({ commentid, onDeleteComment }) => {
return (
<div>
<button onClick={() => onDeleteComment(commentid)} className='btn btn-outline-danger'>X</button>
</div>
)
}
export default DeleteComment
The SubmitComment stores the comment in the Firebase
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import { v4 as uuidv4 } from 'uuid';
import { useDispatch } from 'react-redux';
const SubmitComment = ({ comment, param }) => {
const dispatch = useDispatch();
const onCommentSubmit = () => {
let user;
if (sessionStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(sessionStorage.getItem('user'));
projectFirestore.collection('Comments').doc().set({
id: uuidv4(),
comment,
name: `${user[0].firstName} ${user[0].lastName}`,
userID: user[0].id,
blogID: param,
time: new Date().toLocaleString()
})
dispatch({ type: "SET_TRUE" });
}
}
return (
<div>
<button onClick={() => onCommentSubmit()} className='btn btn-primary'>Add comment</button>
</div>
)
}
export default SubmitComment
In case there is a rout problem here is the code for the routing between the blogs section and the blog + comments section
return (
<Router >
<Route path='/content-page' exact render={(props) => (
<>
<BlogAndCommentPage />
</>
)} />
<Route path='/blogpage' exact render={(props) => (
<>
<div>
<div className="row">
<div className="col-8">
<h1 className='mb-3'>Blog</h1>
</div>
<div className="col-4 mb-3">
<LogoutButton onLogOut={logout} />
<h6 className='float-right mt-4 mr-2'>{displayUser}</h6>
</div>
</div>
{empty ? (<div style={{ color: "red", backgroundColor: "#F39189", borderColor: "red", borderStyle: "solid", borderRadius: "5px", textAlign: 'center' }} className="mb-2">Title and body cannot be blank</div>
) : ("")}
<InputArea getBlogContent={getBlogContent} />
<CreateBlog post={post} onCheckEmptyInputs={checkEmptyTitleAndBody} />
<hr />
<BlogHolder />
</div>
</>
)} />
</Router>
)
If anybody has any clue on why is this happening, please let me know.
Thank you.
As your website is CSR (client side rendering) it doesn't understand the URL in the first execution, you might need to configure a hash router, take a look at:
https://reactrouter.com/web/api/HashRouter
Also, there is a good answer about it here
I am currently working on a app similar to MyAnimeList and I'm using React. I want to click the image of the anime and have it redirect to MyAnimeList for more information. The API i am using is called Jikan APi(https://jikan.moe/). I have seen many ways to do it however most have not worked for my app? I am new to React so im still getting the hang of it.
I am currently using react-bootstrap to for the card.
import React, { useState, useEffect } from 'react';
import { Card, CardColumns, Button } from 'react-bootstrap';
import Nav from '../components/Nav';
import Auth from '../utils/auth';
import { Link } from 'react-router-dom';
import { searchJikanApi } from '../utils/API';
import { saveAnimeIds, getSavedAnimeIds } from '../utils/localStorage';
import video from '../imgs/video.mp4';
import { SAVE_ANIME } from '../utils/mutations';
import { useMutation } from '#apollo/react-hooks';
import Carousel from 'react-elastic-carousel';
import Item from '../components/Carousel/item';
import PopularAnime from '../components/PopularAnime';
import Footer from '../components/Footer';
function Home() {
const [searchedAnimes, setSearchedAnimes] = useState([]);
// create state for holding our search field data
const [searchInput, setSearchInput] = useState('');
// create state to hold saved animeId values
const [savedAnimeIds, setSavedAnimeIds] = useState(getSavedAnimeIds());
const [saveAnime] = useMutation(SAVE_ANIME);
useEffect(() => {
return () => saveAnimeIds(savedAnimeIds);
});
const breakPoints = [
{ width: 1, itemsToShow: 1 },
{ width: 550, itemsToShow: 2, itemsToScroll: 2 },
{ width: 768, itemsToShow: 3 },
{ width: 1200, itemsToShow: 4 },
];
const handleFormSubmit = async (event) => {
event.preventDefault();
if (!searchInput) {
return false;
}
try {
const response = await searchJikanApi(searchInput);
if (!response.ok) {
throw new Error('something went wrong!');
}
const { results } = await response.json();
const animeData = results.map((anime) => ({
animeId: anime.mal_id,
rating: anime.rated || ['No rating to display'],
title: anime.title,
score: anime.score,
description: anime.synopsis,
image: anime.image_url || '',
link: anime.url,
}));
setSearchedAnimes(animeData);
setSearchInput('');
} catch (err) {
console.error(err);
}
};
// function to handle saving an anime to our database
const handleSaveAnime = async (animeId) => {
// find the book in `searchedAnime` state by the matching id
const animeToSave = searchedAnimes.find(
(anime) => anime.animeId === animeId
);
// get token
const token = Auth.loggedIn() ? Auth.getToken() : null;
if (!token) {
return false;
}
try {
await saveAnime({
variables: { ...animeToSave },
});
// if book successfully saves to user's account, save book id to state
setSavedAnimeIds([...savedAnimeIds, animeToSave.animeId]);
} catch (err) {
console.error(err);
}
};
return (
<div className='container'>
<section className='header'>
<Nav></Nav>
<video className='bg-video' autoPlay muted loop>
<source src={video} type='video/mp4' />
Your browser is not supported!
</video>
<div className='heading-primary'>
<form className='search-bar' onSubmit={handleFormSubmit}>
<input
className='heading-search-bar'
name='searchInput'
type='text'
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
/>
<button className='heading-search-btn' type='submit'>
Search
</button>
</form>
</div>
</section>
<div>
<h2>
{searchedAnimes.length
? `Viewing ${searchedAnimes.length} results:`
: 'Search Anime!'}
</h2>
<Link to=''>
<CardColumns className='search-container carousel'>
<Carousel breakPoints={breakPoints}>
{searchedAnimes.map((anime) => {
return (
<Item>
<Card key={anime.animeId} className='anime-card'>
{anime.image ? (
<Card.Img
src={anime.image}
alt={`The cover for ${anime.title}`}
variant='top'
/>
) : null}
<Card.Body>
<Card.Title>{anime.title}</Card.Title>
<p className='small'>Rating: {anime.rating}</p>
<p className='small'>Score: {anime.score}/10</p>
<Card.Text>{anime.description}</Card.Text>
{Auth.loggedIn() && (
<Button
disabled={savedAnimeIds?.some(
(savedAnimeId) => savedAnimeId === anime.animeId
)}
className='btn-block btn-info'
onClick={() => handleSaveAnime(anime.animeId)}
>
{savedAnimeIds?.some(
(savedAnimeId) => savedAnimeId === anime.animeId
)
? 'This anime has already been saved!'
: 'Save this anime!'}
</Button>
)}
</Card.Body>
</Card>
</Item>
);
})}
</Carousel>
</CardColumns>
</Link>
<PopularAnime />
</div>
<Footer />
</div>
);
}
export default Home;
Thank You For Any Help!
try to add onPress to your image to go to our link
<TouchableHighlight
onPress={() => Linking.openURL('https://website.com')}>
<Image
source={{uri: image source}}
/>
</TouchableHighlight>
I am trying to setState of the address and coordinates as given by the "react-places-autocomplete".
I have already stored those components by only issue is in updating it
However, the changes are not updating and even though the program compiles as soon as I select a place it crashes.
Especially for getLatLng there isn't enough documentation for me to understand if I have to access another component within that.
How do I update the state of the address and coordinates via handleSelect?. Thanks!
import React, { Component } from "react";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng
} from "react-places-autocomplete";
export default class Itinerary extends Component {
constructor(props) {
super(props);
this.state = {
address:"",
coordinates:{lat: null,lng: null}
};
}
Create2DArray(rows,columns) {
var x = new Array(rows);
for (var i = 0; i < rows; i++) {
x[i] = new Array(columns);
}
return x;
}
handleChange = address => {
this.setState({ address });
};
handleSelect = address => {
const results = geocodeByAddress(address);
const latLng = getLatLng(results[0]);
this.setState({ coordinates: latLng });
};
render() {
return (
<div className="container">
<div className="row">
<div className="col-md-6 mt-5 mx-auto">
<PlacesAutocomplete
value={this.state.address}
onChange={this.handleChange}
onSelect={this.handleSelect}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<h1>Latitude: {this.state.coordinates.lat}</h1>
<h1>Longitude: {this.state.coordinates.lng}</h1>
<input size="50" height="40"{...getInputProps({ placeholder: "Type address" })} />
<div>
{loading ? <div>...loading</div> : null}
{suggestions.map(suggestion => {
const style = {
backgroundColor: suggestion.active ? "#41b6e6" : "#fff"
};
return (
<div {...getSuggestionItemProps(suggestion, { style })}>
{suggestion.description}
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
</div>
</div>
</div>
);
}
}
geocodeByAddress and getLatLng are asynchronous functions so you have to wait these function until receiving data (read more about geocodeByAddress here). In handleSelect, results[0] is undefined when selecting, so that why your component is crashed. You should try this:
export default class Itinerary extends Component {
...
handleSelect = async address => {
const results = await geocodeByAddress(address);
const latLng = await getLatLng(results[0]);
this.setState({ coordinates: latLng });
};
...
}
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.