Example of issue
import firebase from 'firebase/app';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import { OpenStreetMapProvider } from 'leaflet-geosearch'
import SearchControl from "./SearchControl";
const userId = sessionStorage.getItem('uid')
const fullNames = []
const provider = new OpenStreetMapProvider()
var vendors = []
const VendorMap = (props) => {
firebase.firestore().collection("vendors")
.where('belongsTo', '==', userId)
.get()
.then(querySnapshot => {
querySnapshot.forEach((doc) => {
if(fullNames.includes(doc.data().fullName) == false) {
fullNames.push(doc.data().fullName)
provider.search({query: doc.data().address}).then(function (result) {
vendors.push([doc.data(), [result[0].y, result[0].x]])
}).catch((error) => {
console.log("Error getting collection: ", error)
})
}
})
}).catch((error) => {
console.log("Error getting collection: ", error)
})
return (
<MapContainer center={[51.505, -0.09]} zoom={3} scrollWheelZoom={true}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<SearchControl provider={provider} showMarker={true} showPopup={true} popupFormat={({ query, result }) => result.label} maxMarkers={3} retainZoomLevel={false} animateZoom={true} autoClose={false} searchLabel={"Address to search for..."} keepResult={true} />
{vendors.map((vendor) =>
<Marker key={vendor[0].fullName} position={[vendor[1][0], vendor[1][1]]}>
<Popup>
<span>{vendor[0].fullName}</span>
</Popup>
</Marker>
)}
</MapContainer>
)
}
export default VendorMap
The above link is a video of my issue (its only 10 seconds). React is not immediately loading all of the map markers every time it loads. Sometimes all of the markers load, sometimes only 1 does and I have to click on the navigation bar link for the rest to load.
I am not understanding what the issue could be since I am adding all of the markers one after another to the map. React also doesn't seem to be loading the information from firestore correctly as when I log into the website, I have to completely refresh the website before information begins to load.
If anyone has any insight into what I may be doing wrong and how to fix this, it would be great! I am really new to NodeJS and ReactJS so excuse any common issues on my end.
I am not able to reproduce your issue but the first two things you need to change is:
Place the async operation inside a useEffect hook to load only once when the page loads.
use a useState hook to store the data received from the async operation inside VendorMap component because storing the data in a variable outside the functional component like you do might have unexpected results as React does not know when to update the DOM and compare the old with the new values.
As a result create a local variable using a useState hook
const [vendors, setVendors] = useState([]);
useEffect(() => {
firebase.firestore().collection("vendors")
.where('belongsTo', '==', userId)
.get()
.then(querySnapshot => {
querySnapshot.forEach((doc) => {
if(fullNames.includes(doc.data().fullName) == false) {
fullNames.push(doc.data().fullName)
provider.search({query: doc.data().address}).then(function (result) {
setVendords(yourVariable) // update the vendors array
}).catch((error) => {
console.log("Error getting collection: ", error)
})
}
})
}).catch((error) => {
console.log("Error getting collection: ", error)
})
}, []);
Inside setVendors update vendors variable. I am not sure regarding the data you receive that is the reason I did not put the exact value
If you update fullNames like vendors follow the same pattern as for vendors variable.
Related
I have an array of Notes that I get from my database, the notes objects each have a category assigned to it. There are also buttons that allow the user to filter the notes by category and only render the ones with the corresponding one.
Now, it's all working pretty well but there's one annoying thing that I can't get rid of: whenever I click on any of the buttons: <button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>, the filterNotes() function is only called on the second click. I suspect it has to do something with me calling setState() twice, or maybe with the boolean that I set in the functions, but I tried various combinations to call the function on the first click, but to no avail so far.
Here's my MainArea code:
import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import Note from "./Note";
import axios from "axios"
function CreateArea(props) {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [filterOn, setFilter] = useState(false)
const [note, setNote] = useState({
title: "",
content: "",
category: ''
});
useEffect(() => {
fetch('http://localhost:5000/categories')
.then(res => res.json())
.then(json => setCategories(json))
}, [])
useEffect(() => {
if(fetchB) {
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log(json)
setNotes(json)
setFetch(false)
})
}
}, [fetchB])
function handleChange(event) {
const { name, value } = event.target;
console.log("handleChange called")
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(e){
e.preventDefault();
axios.post("http://localhost:5000/notes/add-note", note)
.then((res) => {
setNote({
category: '',
title: "",
content: ""
})
setFetch(true)
console.log("Note added successfully");
console.log(note)
})
.catch((err) => {
console.log("Error couldn't create Note");
console.log(err.message);
});
}
function expand() {
setExpanded(true);
}
function filterNotes(category){
if(filterOn){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log("filter notes")
setNotes(json)
setNotes(prevNotes => {
console.log("setNotes called with category " + category)
return prevNotes.filter((noteItem) => {
return noteItem.category === category;
});
});
setFilter(false)
})
}
}
return (
<div>
<Header/>
<ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFilter={setFilter} filterOn={filterOn} setFetch={setFetch}/>
<form className="create-note">
{isExpanded && (
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
)}
<textarea
name="content"
onClick={expand}
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows={isExpanded ? 3 : 1}
/>
<select
name="category"
onChange={handleChange}
value={note.category}>
{
categories.map(function(cat) {
return <option
key={cat.category} value={cat.value} > {cat.category} </option>;
})
}
</select>
<button onClick={submitNote}>Add</button>
</form>
<Note notes={notes} setFetch={setFetch}/>
<Footer/>
<button onClick={()=>{setFetch(true)}}>All</button>
</div>
);
}
export default CreateArea;
And ListCategories where I get call the function and get the chosen category from the buttons:
import React, { useState } from "react";
import CreateCategory from "./CreateCategory";
export default function ListCategories(props) {
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
return (
<div className="category-group">
<CreateCategory/>
<div className="btn-group">
{props.categories.map((categoryItem, index) =>{
return(
<button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>
)
})}
</div>
</div>
)
}
I'm not sure what the best practice is with such behaviour - do I get the notes from the database each time as I'm doing now or should I do something completely different to avoid the double-click function call?
Thanks in advance for any suggestions!
Your issue is this function:
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
Understand that in React, state is only updated after the current execution context is finished. So in handleClick() when you call setFiler(), that linked filterOn state is only updated when the rest of the function body finishes.
so when your filterNotes() function is called, when it evaluates filterOn, it is still false, as it was initially set. After this function has executed, the handleClick() function has also finished, and after this, the filterOn state now equals true
This is why on the second click, the desired rendering effect occurs.
There are multiple ways to get around this, but I normally use 'render/don't-render' state by including it as an embedded expression in the JSX:
<main>
{state && <Component />}
</main>
I hope this helps.
You diagnosed the problem correctly. You shouldn't be using state like you would a variable. State is set asynchronously. So, if you need to fetch some data and filter it, do that and THEN add the data to state.
function filterNotes(category){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
const filtered = json.filter((noteItem) => (noteItem.category === category));
setNotes(filtered);
})
}
}
It's not clear to me why you would need the filterOn state at all.
Depending on how your frequently your data is updated and if you plan on sharing data across users, the answer to this question will vary.
If these notes are specific to the user then you should pull the notes on load and then store them in a local state or store. Write actions that can update the state or store so that this isn't coupled with your react UI rendering. Example: https://redux.js.org/ or https://mobx.js.org/README.html.
Then update that store and your remote database accordingly through dispatching actions. This avoids lots of calls to the database and you can perform your filtering client-side as well. You can then also store data locally for offline use through this method so if it's for a mobile app and they lose internet connection, it'll still render. Access the store's state and update your UI based on that. Specifically the notes and categories.
If you have multiple users accessing the data then you'll need to look at using websockets to send that data across clients in addition to the database. You can add listeners that look for this data and update that store or state that you will have created previously.
There are many approaches to this, this is just an approach I would take.
You could also create a context and provider that maintains your state on the first load and persists after that. Then you can avoid passing down state handlers through props
I'm working on web scraping a news website to show top headlines and URLs. In my backend, I put each title and URL in an object within an array, which works fine. When I fetch this information in React, it is not working as expected.
I used the useState hook to initialize an empty array, where I would put each object containing the title and URL. Then, I map through that array and render each title and URL to the page.
When I refresh the webpage, it takes several seconds for each title and URL to pop up, and additionally, they are all the same title and URL.
It seems that my array is not being updated properly by putting in the same article information each time I set the state. However, I do not understand why it is taking so long for all the information to show up on the webpage, and do not know how to make it a faster process. I want all the information to be on the page after the user hits refresh, not appear after several seconds at a time. Could anyone help me see where I'm going wrong?
Here is my code:
import {useState} from 'react';
const News = () => {
const [articles, setArticles] = useState([])
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => {
data.forEach(article => {
setArticles([...articles, {
title: article.article_title,
url: article.article_url}])
})
})
.catch(err => console.log(err))
return (
<div>
{articles.map(article => {
return (
<div className="indv-article">
<h1 className="article-title" key={article.title}>{article.title}</h1>
<p className='article-url' key={article.url}>{article.url}</p>
</div>);
})}
</div>
)
}
export default News
Couple of things that may solve your issues.
First: In React, all side-effects (such as data fetching, for example) should be handled inside a useEffect hook.
Second: As stated in #Khorne07's answer, the key attribute should be on the root DOM node that is being returned from the map.
Third: I don't really know the purpose of looping through your data to set the state. If the reason you are doing this is because the response contains other information that you are not interested to display and you just want the title and url for each article, I suggest you to create an adapter function that will receive this data as a parameter and return just the information that you are interested in.
Additional: You can use a loading state to show a loading indicator while the data is being fetched and improve user experience.
Putting it all together:
const News = () => {
const [articles, setArticles] = useState([])
const [loading, setLoading] = useState(false)
const adaptArticles = (data) => {
return data.map(({ article_title, article_url }) => ({
title: article_title,
url: article_url
}))
}
useEffect(() => {
setLoading(true)
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => setArticles((prevArticles) => prevArticles.concat(adaptArticles(data))))
.catch(err => console.log(err))
.finally(() => {
setLoading(false)
})
}, []) //Insert the corresponding dependencies (if any) in the dependencies array so the useEffect hook gets executed when any of these dependencies change.
if(loading) //Return some loading indicator, like a Spinner for example.
return (
<div>
{articles.map(article => {
return (
<div className="indv-article" key={article.title}>
<h1 className="article-title">{article.title}</h1>
<p className='article-url'>{article.url}</p>
</div>);
})}
</div>
)
}
Edited:
You have some errors on your current code:
First of all, and, as mentioned by the accepted answer, all side effects like fetch calls should be inside a useEffect hook.
The second error is related to the way you are updating your state array. When your new state depends on the previous state value, you should use the callback function inside your setState function, in order to have your data correctly synchronized with the previous value. And in this particular example you are also calling a setState function inside a loop, which is a bad idea and can potentially drive your app into unexpected behavior. The best approach is described in the code snippet bellow.
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => {
const articlesArray = []
data.forEach(article => {
articlesArray.push({
title: article.article_title,
url: article.article_url
})
setArticles(currentArticles => [...currentArticles, ...articlesArray])
})
})
.catch(err => console.log(err))
And on the map function, the key attribute should be on the root node you are returning:
{articles.map(article => {
return (
<div className="indv-article" key={article.title}>{article.title}>
<h1 className="article-title"</h1>
<p className='article-url'</p>
</div>);
})}
I am programming a react application where I need to make a single async api call, save the result to a state variable and display the result. I am using an axios instance where the response of the api call is a nested object. For example:
{
kanji:
{character: "", stroke:""},
quote: "string"
}
So far I have the following code where I am able to console.log the homeData object successfully. However I get the error: 'TypeError: homeData is undefined'.
const Home = () =>{
const [homeData, setHomeData] = useState({})
const getHomeData = async()=>{
instance.get("/data")
.then(res =>{
setHomeData(res.data)
})
.catch(error =>{
console.log(error)
})
}
useEffect(()=>{
getHomeData()
}, [])
console.log(homeData)
return(
<>
<p>{homeData}<p>
</>
)
}
This seems like a promise issue but I am not sure how to fix it. Thank you for your help in advance.
This is not a promise issue, it has to due with the order in which the lifecycle methods run. useEffect runs after the component renders. This means when the component first renders, the homeData is an empty object. homeData will be present in the second render. The following code first checks if homeData exists, then if it exists, it displays the kanji character. Note also, you cant just display a raw object into the dom so you will have to access the homeData by property for it to display
const Home = () =>{
const [homeData, setHomeData] = useState({})
const getHomeData = async()=>{
instance.get("/data")
.then(res =>{
setHomeData(res.data)
})
.catch(error =>{
console.log(error)
})
}
useEffect(()=>{
getHomeData()
}, [])
console.log(homeData)
return(
<>
<p>{homeData && homeData.kanji.character}<p>
</>
)
}
I'm having an issue when trying to save to State an axios API call. I've tried
useState set method not reflecting change immediately 's answer and many other and I can't get the state saved. This is not a duplicate, because I've tried what the accepted answer is and the one below and it still doesn't work.
Here's the (rather simple) component. Any help will be appreciated
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // returns '[]'
});
}, []); // If I set 'widgets' here, my endpoint gets spammed
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Welcome to stackoverflow, first thing first the setting call is incorrect you must use spread operator to combine to array into one so change it to setWidgets([...widgets, ...data]); would be correct (I assume both widgets and data are Array)
second, react state won't change synchronously
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // <--- this will output the old state since the setWidgets above won't do it's work till the next re-render
so in order to listen to the state change you must use useEffect hook
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
this will console log anytime widget changes
the complete code will look like this
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
setWidgets([...widgets, ...data])
});
}, []);
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Try:
setWidgets(data);
istead of
setWidgets(widgets, data);
Your widgets.map() probably fails because there isn't much to map over when the component is being rendered.
You should update it with a conditional like so, just for clarity:
widgets.length>0 ? widgets.map(...) : <div>No results</div>
And your call to setWidgets() should only take one argument, the data:
setWidgets(data)
or if you want to merge the arrays use a spread operator (but then you need to add widgets as the dependency to the useEffect dependency array.
setWidgets(...widgets, ...data)
You might also have to supply the setWidgets hook function to the useEffect dependency array.
Let me know if this helps..
Im new to react and building an app to better understand it. Im currently using the Google maps api to render a map and want to render multiple markers to the map.
Im having to use a geocode api to get the lat and longitude for each of my locations so I can display them on the map as markers. Currently my code isn't displaying anything to the screen and I think its because the map is trying to render the markers with an empty state variable before the fetch calls are completed.
I have tried to search for an answer but cant find anything that is similar to what Im trying to do.
Here is my code
import React, { useContext, useEffect, useState } from 'react';
import { GoogleMap, useLoadScript, Marker } from "#react-google-maps/api"
import Settings from "../Settings"
import mapSyles from "./MapStyles"
import { LogContext } from '../divelog/DiveLogProvider';
export const MapRender =(props) => {
const {diveLogs} = useContext(LogContext)
const [latLong, setLatLong] = useState()
//get the logs location and run that through api to get lat and long
useEffect(()=>{
let latLongs = []
diveLogs.map(dl =>{
return fetch(`http://api.positionstack.com/v1/forward?access_key=MYKEY&query=${dl.location}&limit=1
`)
.then(res => res.json())
.then(parsedRes => {
latLongs.push(parsedRes.data[0])
})
})
setLatLong(latLongs)
console.log(latLong)
},[diveLogs])
//here I want to map through the state variable and display all of the markers on the map
return (
<div>
<GoogleMap
mapContainerStyle={mapContainerStyle}
options={options}
zoom={1}
center={center}
>
{
//this is where I map through the state variable
latLong.map(l =>(
<Marker key={l.lat}
position ={{lat: l.latitude, lng: l.longitude}}
/>
))
}
</GoogleMap>
</div>
)
}
you can update the state right inside your response . like this
diveLogs.map(dl =>{
return fetch(`http://api.positionstack.com/v1/forward?access_key=MYKEY&query=${dl.location}&limit=1
`)
.then(res => res.json())
.then(parsedRes => {
setLatLong( prevLatLongs => [...prevLatLongs ,parsedRes.data[0]] )
})
})
Yo need update the state when the fetch result is ready:
useEffect(()=>{
diveLogs.map(dl =>{
return fetch(`http://api.positionstack.com/v1/forward?access_key=MYKEY&query=${dl.location}&limit=1
`)
.then(res => res.json())
.then(parsedRes => {
setLatLong((prev)=> [...prev, parsedRes.data[0]])
})
})
},[diveLogs])