Promise only resolves correctly on page refresh - javascript

I am playing around with an API that gets a list of Pokemon and corresponding data that looks like this.
export function SomePage() {
const [arr, setArray] = useState([]);
useEffect(() => {
fetchSomePokemon();
}, []);
function fetchSomePokemon() {
fetch('https://pokeapi.co/api/v2/pokemon?limit=5')
.then(response => response.json())
.then((pokemonList) => {
const someArray = [];
pokemonList.results.map(async (pokemon: { url: string; }) => {
someArray.push(await fetchData(pokemon))
})
setArray([...arr, someArray]);
})
}
async function fetchData(pokemon: { url: string; }) {
let url = pokemon.url
return await fetch(url).then(async res => await res.json())
}
console.log(arr);
return (
<div>
{arr[0]?.map((pokemon, index) => (
<div
key={index}
>
{pokemon.name}
</div>
))
}
</div>
);
}
The code works(kind of) however on the first render the map will display nothing even though the console.log outputs data. Only once the page has been refreshed will the correct data display. I have a feeling it's something to do with not handling promises correctly. Perhaps someone could help me out.
TIA
Expected output: Data populated on initial render(in this case, pokemon names will display)

The in-build map method on arrays in synchronous in nature. In fetchSomePokemon you need to return a promise from map callback function since you're writing async code in it.
Now items in array returned by pokemonList.results.map are promises. You need to use Promise.all on pokemonList.results.map and await it.
await Promise.all(pokemonList.results.map(async (pokemon: { url: string; }) => {
return fetchData.then(someArray.push(pokemon))
}));

On your first render, you don't have the data yet, so arr[0] doens't exist for you to .map on it, so it crashes. You need to check if the data is already there before mapping.
Using optional chaining, if there's no data it will not throw an error on your first render and it will render correctly when the data arrive and it re-renders.
...
return (
<div>
{arr[0]?.map((pokemon, index) => (
<div key={index}>{pokemon.name}</div>
))}
</div>
);
}

in
useEffect(() => { fetchSomePokemon(); }, []);
[] tells react there is no dependencies for this effect to happen,
read more here https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

One way to solve your issues is to await the data fetching in useEffect().
Here's a POC:
export function Page() {
const [pokemon, setPokemon] = useState([]);
// will fetch the pokemon on the first render
useEffect(() => {
async function fetchPokemon() {
// ... logic that fetches the pokemon
}
fetchPokemon();
}, []);
if (!pokemon.length) {
// you can return a spinner here
return null;
}
return (
<div>
{pokemon.map(item => {
// return an element
})}
</div>
);
}

Related

ReactJS how to update page after fetching data

I'm new to ReactJS and I'm now trying to do an interactive comments section (taken from frontendmentor.io), but the App component just doesn't show what it's supposed to show
This is my App component:
function App() {
const [data, setData] = useState([]);
useEffect(() => {
const getComm = async () => {
await fetchData();
};
getComm();
}, []);
console.log(data);
const fetchData = async () => {
const res = await fetch("db.json").then(async function (response) {
const comm = await response.json();
setData(comm);
return comm;
});
};
return (
<Fragment>
{data.length > 0 ? <Comments data={data} /> : "No Comments to Show"}
</Fragment>
);
}
export default App;
The console.log(data) logs two times:
the first time it's an empty Array;
the second time it's the Array with my datas inside.
As it follows:
If I force the App to print the Comments it just says that cannot map through an undefined variable
This is my Comments component:
function Comments({ data }) {
return (
<div>
{data.map((c) => (
<Comment key={c.id} />
))}
</div>
);
}
export default Comments;
I'm wondering why the page still displays No Comments to Show even if the log is correct
#Cristian-Irimiea Have right about response get from fetch. Response is an a object and can't be iterate. You need to store in state the comments from response
But you have multiple errors:
Take a look how use async function. Your function fetchData looks bad.
// Your function
const fetchData = async () => {
const res = await fetch("db.json").then(async function (response) {
const comm = await response.json();
setData(comm);
return comm;
});
};
// How can refactor
// fetchData function have responsibility to only fetch data and return a json
const fetchData = async () => {
const response = await fetch("./db.json");
const body = await response.json();
return body;
};
You are updating state inside fetch function but a good solution is update state then promise resolve:
useEffect(() => {
// here we use .then to get promise response and update state
fetchData().then((response) => setData(response.comments));
}, []);
The initial state of your data is an array.
After you fetch your data from the response you get an object. Changing state types is not a good practice. You should keep your data state as an array or as an object.
Considering you will keep it as an array, you need use an array inside of setData.
Ex.
comm && Array.isArray(comm.comments) && setData(comm.comments);
As for your Comments component you should consider expecting an array not an object.
Ex.
function Comments(data) {
return (
<div>
{data.map((c) => (
<Comment key={c.id} />
))}
</div>
);
}
export default Comments;

why wont jsx render firestore data

I'm trying to GET data from firestore and render it, in the most basic way possible. I can console.log the data, but as soon as I try to render it via JSX, it doesn't. Why is this?
import React from 'react'
import { useState, useEffect } from 'react'
import {db} from '../../public/init-firebase'
export default function Todo() {
const [todo, setTodo] = useState()
const myArray = []
//retrive data from firestore and push to empty array
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
}
useEffect(() => {
getData()
}, [])
return (
<>
<div>
<h1>Data from firestore: </h1>
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
</div>
</>
)
}
First, change myArray to State like this:
const [myArray, setMyArray] = useState([]);
Every change in myArray will re-render the component.
Then, to push items in the State do this:
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
setMyArray(oldArray => [...oldArray, doc.id])
})
})
}
You're just pushing the ID in myArray, so when to show try like this:
{myArray.map((id) => {
console.log('hi')
return <h1 key={id}>{id}</h1>
})}
If you look closely,
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
those are curly braces {}, not parenthesis (). This means that although doc exists, nothing is happening since you are just declaring <h1>{doc.id}</h1>. In order for it to render, you have to return something in the map function. Try this instead:
{myArray.map((doc) => {
console.log('hi')
return <h1>{doc.id}</h1>
})}
In order to force a "re-render" you will have to use the hooks that you defined
https://reactjs.org/docs/hooks-intro.html
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
// in case the request doesn't fail, and the array filled up with correct value using correct keys:
// i'd setState here, which will cause a re-render
setTodo(myArray)
}

React, using useEffect to updata data, a data is a Array, but when I use map or Object.keys(data).map, both are not work. How to make it work?

I used the useEffect to updata the data, I can see the data is a array in DEV tool, but when I using map to traverse the data, will show be that data.map is not a function. If I use Object.keys(data).map, there are not thing was showed, but no error...
How to fix ...?
export default function List() {
let [data, setData] = useState();
useEffect(async () => {
let result = await loadList();
console.log(result.data.data);
setData({ data: result.data.data });
return;
}, data);
return (
<div>
{
(data) && data.map((item) => {
return <div key={item.id} >{item.title} author: {item.author} Creact time: { item.creactetime}</div>
})
}
</div>
)}
useEffect does not allow the use of an async function. Instead you should just use the then method when calling your loadList function and set the data in the callback.
It seems that useEffect doesn't need a dependency as it only needs to get the data on the first render, not when data changes. The latter would result in an infinite loop (Update data => do something when data updates => update data)
In setData just pass the result.data.data to update data, not within another object. Otherwise the data will not be an array, but an object.
export default function List() {
let [data, setData] = useState(null);
useEffect(() => {
loadList().then((result) => {
setData(result.data.data);
});
}, []);
return (
<div>
{data &&
data.map((item) => (
<div key={item.id}>
{item.title} author: {item.author} Creact time: {item.creactetime}
</div>
))}
</div>
);
}
useEffect can not have an async callback you need to wrap it like this
useEffect(()=>{
const fetch = async () => {
const result = await loadList();
setData(result.data.data);
}
fetch();
}, []);
data must be an array if you want to map on it, you can try to set directly data with result.data.data if it's an array, like following :
useEffect(async () => {
let result = await loadList();
setData(result.data.data);
}, []);

Object value is undefined but it's there React

I have the following code :
export default function ProjectView({filteredProjects,revenueGroups}) {
const [projects,setProjects] = useState();
useEffect(() => {
const aap = filteredProjects.map(filteredObject => {
getProjectFields(filteredObject.projectId).then(res => {
filteredObject.rekt = res.data[0].id;
})
return filteredObject;
})
setProjects(aap);
},[filteredProjects])
And the rendered component :
return (
<div className='main'>
{projects.map(project => (
<div className='view-container' key={project._id}>
{console.log(project)}
</div>
))}
</div>
)
This works fine , when i console.log(project) like above it shows the following :
{
projectName: "privaye"
rekt: "project:1b1126ebb28a2154feaad60b7a7437df"
__proto__: Object
}
when i console.log(projectName) it shows the name, but when i console.log(project.rekt) it's undefined...
eventhough its there when i console.log(project)
EDITED
I didn't notice the was a promise inside :P
useEffect(() => {
fetchThings()
},[filteredProjects])
const fetchThings = async () => {
const promArr = filteredProjects.map(filteredObject => {
return getProjectFields(filteredProject.project.id)
})
const filteredObjects = await Promise.all(promArr)
const projectsMod = filteredObjects.map(fo => ({
...fo,
rekt: fo.data[0].id,
}))
}
Maybe an approach like this will help you with the asyncronous problem
console.log isn't rendering anything so maybe React doesn't try to refresh this part of the DOM when projects state is updated.
You can stringify the object to check the content of the variable inside the return
return (
<div className='main'>
{projects.map(project => (
<div className='view-container' key={project._id}>
<span>{ JSON.stringify(project) }</span>
</div>
))}
</div>
)
You are running promise, after this promise will return value you set rekt, but promise will work god knows when, and most probably you check value before this promise resolved.
getProjectFields(filteredObject.projectId).then(res => {
// THIS CODE CAN RUN AFTER RENDERING
filteredObject.rekt = res.data[0].id;
})
so first you wait till promises will complete and only then set new state
const ourPreciousPromises = filteredProjects.map(filteredObject =>
getProjectFields(filteredObject.projectId).then(res => {
filteredObject.rekt = res.data[0].id;
return {...filteredObject}
})
})
Promise.all(ourPreciousPromises).then(newObjects =>
setProjects(newObjects)
)

Setting state in getDetails() occurs infinite loop

this.state = {
data: [],
details: []
}
componentDidMount() {
this.getDetails()
this.getCountries()
}
getCountries() {
Utils.rest('POST', 'https:///api-spot-get-all', {
country: '',
windProbability: ''
}).then(async (r) => {
const data = await r.json();
this.setState({
data: data.result
})
}).catch(err => {
console.log(err.message);
});
}
`getDetails() {
return new Promise((resolve, reject) => {
let details_list = [];
this.state.data.map(item => {
return (
Utils.rest('POST', 'https:///api-spot-get-details', {
spotId: item.id
})
.then(async (r) => {
const details_item = await r.json()
console.log(`Loaded ${details_list.length} item ...(of ${this.state.data.length})`);
if (details_list.length === this.state.data.length) {
await resolve(details_list)
}
details_list.push(details_item.result);
})
);
})
})
}`
render() {
return (
{
this.state.data.map((item, key) => {
return (
{item.id}
{item.id}
);
})
}
Here is my code. After first call I am receiving id and passing it as input to second call
I think this is happening because you're calling this.getCountries() in the render function. So the function is called in every render, that causes a new request that sets a new state, which will trigger a new render an so on, creating an infinite loop. So, if you delete the function calling from the render function it should work.
This is a basic example:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const myarray=[1,2,3,4,5]
const mytable=myarray.map((item,key)=>{return(
<table key={key}>
<tr><td>id</td><td>{item}</td></tr>
</table>)
})
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{mytable}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can create a const in the render and use after in return so you can tryin your code to do something like that:
render() {
const myComponent= this.state.data.map((item, key) => { return (
<div key={key}>
<span>{item.it}</span>
</div>
) });
return (
{myComponent}
)
}
I used and is just for example you can use what structor you want as in the first example I used table...
Please note that you are calling getDetails method in render. render is not a good place to add methods which modify the internal state. please check the react doc for additional details.
There are a lot of strange things there. First of all, getDetails returns a Promise, but the promise is not resolved anywhere. Its usage should be something like:
getDetails()
.then(data => {
// do something with the data
}, error => {
// manage here the error
}):
Also, this.state.data.map should be this.state.data.forEach and delete the return from inside, because you don't need to return anything outside
On the other hand, there's an issue with getCountries. Its name sais it's a GET, but the API call sends a POST.
After that's clarified, inside getDetails you're using the data retrieved in getCountries, so its call should be inside the request resolving inside getCountries or either change getCountries to a Promise and do something like:
this.getCountries()
.then(data => {
this.getDetails();
});
You don't care when the getDetails call ends, so it doesn't need to return a Promise.
And, in the end, the render function should look more like this:
render() {
return (
<div>
{this.state.data.map((item, key) =>
<div key={key}>{item.id} - {item.id}</div>
)}
</div>
)
}
After this it should work properly, more or less. I have to warn you, though. Probably you would need to do something easier to become familiar with React's flow and how to properly work with state and JS's asynchronous functions.

Categories

Resources