nutritionData.map is not a function - reactjs

I'm currently working on a nutrition app and I'm trying to get my response to render on the screen by mapping over the response array but I'm getting an error that it is not a function.
const [nutritionData, setNutrition] = useState([]);
useEffect( () => {
getNutrition();
}, []);
const getNutrition = async () => {
const response = await fetch(`https://api.edamam.com/api/food-database/parser?nutrition-type=logging&ingr=red%20apple&app_id=${APP_ID}&app_key=${APP_KEY}`)
const data = await response.json();
console.log(data.hints[0].food.nutrients);
setNutrition(data.hints[0].food.nutrients);
};
return (
<div
{nutritionData.map(nutrients =>(
<Nutrition calories={nutrients[0]} carbs=
{nutrients.nutrients[3]} />
))}
</div>
);
};

nutritionData may be used as an array but its prototype is not Array.prototype so you can't use functions like map(), forEach()...
You can check the comparison nutritionData.__proto__ === Array.prototype to figure out.
Hope this can help.

Related

How to display an array in JSX from a function in React?

I implemented a function where I fetch all Docs from a Firebase collection on a click.
Now I want to display each doc I fetched in a <div> container in JSX. When I try to take the array and display it, I´m getting the error that the array is not found.
This is my code:
async function getAllDivs(){
const querySnapshot = await getDocs(collection(db, "Div"))
const allDivs = [];
querySnapshot.forEach(doc => {
allDivs.push(doc.data().DivContent);
});
}
You would have to return the array from the function, because of the "scope".
Example:
//your current function
async function getAllDivs(){
const querySnapshot = await getDocs(collection(db, "Div"));
return querySnapshot.map((doc) => doc.data().DivContent);
}
//your component
let divs = getAllDivs(); //you can now use "divs" in this scope
return (
<>
divs.map((current_div) => { <div>{current_div}</div> })
</>
)
Also, I suggest against pushing data to an array labeled as const, as it could be confusing for someone else reading your code.
I think you could use something like this:
const MyComponent = () => {
const [docs, setDocs] = useState();
const onClickHandler = async () => {
const docs = await getDocs(collection(db, "Div"));
setDocs(docs);
}
return (
<>
<button onClick={onClickHandler}>Get docs</button>
{docs && docs.map(doc => (
<div>{doc.data().DivContent}</div>
))}
</>
)
}
If DivContent contains HTML you can use dangerouslySetInnerHTML.

Mapping into firestore from React

I am new to react and firebase/firestore.
I am trying to map into what I believe to be a nested firestore value. I am able to pull each value individually
function Pull() {
const [blogs,setBlogs]=useState([])
const fetchBlogs=async()=>{
const response=firestore.collection('customer');
const data= await response.get();
data.docs.forEach(item=>{
setBlogs(data.docs.map(d => d.data()))
console.log(data)
})
}
useEffect(() => {
fetchBlogs();
}, [])
return (
<div className="App">
{
blogs.map((items)=>(
<div>
<p>{items[1].name}</p>
</div>
))
}
</div>
);
}
I have been trying to map twice to get into the string inside the collection, yet I have had no luck.
My FireStore collection
https://drive.google.com/file/d/1Erfi2CVrBSbWocQXGR5PB_ozgg9KEu12/view?usp=sharing
Thank you for your time!
If you are iterating a data.docs array and enqueueing multiple state updates then you will want to use a functional state update to correctly enqueue, and update from the previous state.
const fetchBlogs = async ( )=> {
const response = firestore.collection('customer');
const data = await response.get();
data.docs.forEach(item => {
setBlogs(blogs => blogs.concat(item.data()))
});
}
or you can map the data.docs to an array of items and update state once.
const fetchBlogs = async ( )=> {
const response = firestore.collection('customer');
const data = await response.get();
setBlogs(blogs => blogs.concat(data.docs.map(item => item.data())));
}
try changing the foreach to a snapshot like this:
data.docs.onSnapshot(snapshot=>{
setBlogs(snapshot.docs.map(d => d.data()))
console.log(data)
})
ive used it like this in the past multiple times
and it has worked. If it doesnt work, the instagram clone tutorial on youtube by Clever Programmer goes over this well.

Chaining useEffects with early return

Example to make the context clear:
I am trying to render a component with two sets of data coming from API calls. I am also returning early if the first API call fails. The second API call depends on the data of the first API result. I don't want to combine both effects because that would mean the whole of component does not render till I get bot API results.
This is the psuedo code
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
useEffect(() => {
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [data1]);
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Problem
The above code does not work because you cannot add a useEffect conditionally (the early return messes it up)
Trying to find the best workaround for this problem.
Just creat a component for dataFromEffect2:
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () =>
setDataFromEffect1(await (await fetch("/api1")).json());
callApi1();
}, []);
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
return (
<div>
{dataFromEffect1}
<Component data1={data1} dataFromEffect2={dataFromEffect2} setDataFromEffect2={setDataFromEffect2} />
</div>
);
};
const Component = ({ data1, dataFromEffect2, setDataFromEffect2 }) => {
useEffect(() => {
const callApi2 = async () =>
setDataFromEffect2(
await (await fetch("/api2", { headers: data1 })).json()
);
callApi2();
}, [data1]);
return <div>{dataFromEffect2}</div>;
};
You can try something like this. It will kill both useEffects, but it will not run the second one unless it retrieved data from the first. Also, I did not fix this in your code but you should not use async code within useEffect. This can lead to memory leaks and unnexpected bugs. You are also not cleaning up the fetch from within the useEffect. Academind has a nice blog explaining how to fix this and what will happen if you keep the code like this https://academind.com/tutorials/useeffect-abort-http-requests/
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
useEffect(() => {
if(!dataFromEffect1.length) return;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [dataFromEffect1]);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Two options ( I went with the second one for now):
create a separate component for abstracting the second useEffect as suggested by #viet in the answer below.
move the useEffect above the early return, but I will have to duplicate the if conditions inside the effect. As described by in the answer below.

React, unexpected multiple result when using map and fetch

I am building Weather App, my idea is to save city name in localStorage, pass a prop to child component, then iterate using map and display each in seperate child of the first child
The problem is that displayed data doubles/triples on render(depending on component when render occurs) so when I have for example city London and add city Berlin it will render:
London,London,Berlin
The problem is not in AddCity component, it's working correctly but in this mix of asynchronous setState/fetching and maping
Please see the code below
App(parent component)
const App = () => {
const [cities, setCities] = useState([]);
const addCity = (newCity)=>{
console.log('adding')
setCities([...cities, newCity]);
let cityId = localStorage.length;
localStorage.setItem(`city${cityId}`, newCity);
}
useEffect(() => {
loadCityFromLocalStore()
}, [])
const loadCityFromLocalStore =()=>{
setCities([...cities, ...Object.values(localStorage)])
}
return (
<div>
<Header />
<AddCity addCity={addCity}/>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
DisplayWeather (first child)
const DisplayWeather = ({displayWeather}) => {
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=>[...fetchData , data]));
})
}, [displayWeather])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
Weather component
const Weather = ({data}) => {
return (
<li>
{data.name}
</li>
)
}
It looks like the problem comes from calling setFetchData for cities that you already added previously.
One easy way to fix it would be to store fetch data as an object instead of a dictionary so that you just override the data for the city in case it already exists (or maybe even skip the fetch as you already have the data).
For example:
const [fetchData, setFetchData] = useState({});
useEffect(() => {
displayWeather.map(async city=>{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
})
}, [displayWeather])
Then, to map over fetch data you can just use Object.values:
return (
<>
{Object.values(fetchData).map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
If you want to skip already fetched cities you can do something like this instead:
useEffect(() => {
displayWeather.map(async city=>{
if (!fetchData[city]) {
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
}
})

Problem occurs when check conditions in map function React

I am trying to get data from the backend and display the data in the frontend. To do that I tried this code.
function Posts() {
const [notes, getNotes] = useState([]);
useEffect(()=>{
getAllNotes();
}, []);
const getAllNotes = async () => {
await axios.get(`/buyerPosts`)
.then ((response)=>{
const allNotes=response.data.existingPosts;
getNotes(allNotes);
})
.catch(error=>console.error(`Error: ${error}`));
}
console.log(notes);
const buyerId=(localStorage.getItem("userId"));
console.log(buyerId);
const [offers, getOffers] = useState([]);
useEffect(()=>{
getAllOffers();
}, []);
const getAllOffers = async () => {
await axios.get(`/viewPendingSellerOffers`)
.then ((response)=>{
const allNotes=response.data.existingOffers;
getOffers(allNotes);
})
.catch(error=>console.error(`Error: ${error}`));
}
console.log(offers);
const wasteItem = offers?.filter(wasteItem => wasteItem.status==='accepted' && wasteItem.buyerId===buyerId && wasteItem.wasteItemsListId==='completePost');
console.log(wasteItem);
return(
<main className="grid-b">
{notes.map((note,index)=> {
if(note._id===wasteItem.postId)
return (
<article>
<div className="text-b">
<h3>Post ID: {index + 1}</h3>
<p>Location: {note.address}</p>
<p>Post Type: {note.postType}</p>
<p>Address: {note.address}</p>
<p>Telephone No: {note.contact}</p>
</div>
</article>
);
})}
</main>
);
}
export default Posts;
Hare, I call the first API and get a length 7 array of objects. This is an image of this result.
Then I call a second API and get a length 6 array of objects. This is an image of this result.
Then I try to filter second API call results like this const wasteItem = offers?.filter(wasteItem => wasteItem.status==='accepted' && wasteItem.buyerId===buyerId && wasteItem.wasteItemsListId==='completePost'); and I get length 2 array of objects as the result of this filter function.
Then I try to map the first API call data in a map function. To do that I used this condition if(note._id===wasteItem.postId). But this map function is not working. Maybe it does not work because wasteItem is an array of objects. How do I solve this problem?
wasteItem is an array of objects, but you treated it as object here if(note._id===wasteItem.postId). You would need to iterate through wasteItem array first, or use find().
{notes.map((note,index)=> {
if(wasteItem.find(o=>o.postId === note._id) !== undefined)
return (
<article>
...
</article>
);
})}

Resources