Can I use a loop inside a react hook? - javascript

Can i do this:
const [borderCountries, setBorderCountries] = useState([])
useEffect(() => {
country.borders.forEach(c => {
fetch(`https://restcountries.eu/rest/v2/alpha/${c}`)
.then(res => res.json())
.then(data => setBorderCountries([...borderCountries,data.name]))
})
}, [])
Country borders is a prop passed to the component. If not what can I do?

You can, but not quite like that, for a couple of reasons:
Each fetch operation will overwrite the results of the previous one, because you're using borderCountries directly rather than using the callback version of setBorderCountries.
Since the operation depends on the value of a prop, you need to list that prop in the useEffect dependencies array.
The minimal change is to use the callback version:
.then(data => setBorderCountries(borderCountries => [...borderCountries,data.name]))
// ^^^^^^^^^^^^^^^^^^^
...and add country.borders to the useEffect dependency array.
That will update your component's state each time a fetch completes.
Alternatively, gather up all of the changes and apply them at once:
Promise.all(
country.borders.map(c =>
fetch(`https://restcountries.eu/rest/v2/alpha/${c}`)
.then(res => res.json())
.then(data => data.name)
})
).then(names => {
setBorderCountries(borderCountries => [...borderCountries, ...names]);
});
Either way, a couple of notes:
Your code is falling prey to a footgun in the fetch API: It only rejects its promise on network failure, not HTTP errors. Check the ok flag on the response object before calling .json() on it to see whether there was an HTTP error. More about that in my blog post here.
You should handle the possibility that the fetch fails (whether a network error or HTTP error). Nothing in your code is currently handling promise rejection. At a minimum, add a .catch that reports the error.
Since country.borders is a property, you may want to cancel any previous fetch operations that are still in progress, at least if the border it's fetching isn't still in the list.
Putting #1 and #2 together but leaving #3 as an exercise for the reader (not least because how/whether you handle that varies markedly depending on your use case, though for the cancellation part you'd use AbortController), if you want to update each time you get a result
const [borderCountries, setBorderCountries] = useState([]);
useEffect(() => {
country.borders.forEach(c => {
fetch(`https://restcountries.eu/rest/v2/alpha/${c}`)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
return res.json();
})
.then(data => setBorderCountries(borderCountries => [...borderCountries, data.name]))
// ^^^^^^^^^^^^^^^^^^^
.catch(error => {
// ...handle and/or report the error...
});
});
}, [country.borders]);
// ^^^^^^^^^^^^^^^
Or for one update:
const [borderCountries, setBorderCountries] = useState([]);
useEffect(() => {
Promise.all(
country.borders.map(c =>
fetch(`https://restcountries.eu/rest/v2/alpha/${c}`)
.then(res => res.json())
.then(data => data.name)
})
)
.then(names => {
setBorderCountries(borderCountries => [...borderCountries, ...names]);
})
.catch(error => {
// ...handle and/or report the error...
});
}, [country.borders]);
// ^^^^^^^^^^^^^^^

You can but it's Not a good practice.

Related

console log of returned value shows a unusable response even after chaining a then to it

Im trying to return a promise is a javascript file. However, there is a weird issue. So when I console log the returned value within the function, it shows the following:
const id = getAccounts()
.then(res => res.find(acc => acc.type === ACCOUNT_TYPES.STARTER))
.then((res) => { return res.id });
console.log(id.then(res => res))
Is there anything I am missing? Have been dealing with this and research for the whole day. If anyone can help, I would highly appreciate it!
Updated section:
const initialState = {
currentAccountId: id.then((res) => { return res; }) || ''
};
The return value of calling a Promise's .then is always another Promise. By setting currentAccountId to id.then, it will always be a Promise.
You need to call this.setState from inside the Promise's resolve function:
componentDidMount() {
getAccounts()
.then(res => res.find(acc => acc.type === ACCOUNT_TYPES.STARTER))
.then((res) => { this.setState({ currentAccountId: res }); });
}
Use componentDidMount, like the React docs suggest, to initiate an async request. "If you need to load data from a remote endpoint, this is a good place to instantiate the network request."
Original answer
id.then will always return a new Promise, and that's what you are logging. To log the actual value you can move the console.log inside the resolve function:
id.then(res => console.log(res))

React how to call 1 promise after the previous promise is finish?

I have a React hook with this structure. What I want to do is, after finish calling getUserJoinedSlotList() and getting the result, then I want to call getAllAvailableSlot() both set the result into the useState hooks.
const [joinedSlotList, setJoinedSlotList] = useState(null)
const [availableSlotList, setAvailableSlotList] = useState(null)
const [isAllSlotLoading, setIsAllSlotLoading] = useState(true)
const getJoinedList = () => {
getUserJoinedSlotList()
.then(res => {
setIsLoading(false)
setJoinedSlotList(res.joined_slot)
})
.catch(error => {
setIsLoading(false)
setErrorMsg(error.message)
})
}
const getAvailableSlotList = () => {
getAllAvailableSlot()
.then(res => {
setIsAllSlotLoading(false) // this setting not working, at the 2nd API call
setAllAvailableSlotList(res.slot)
})
.catch(error => {
setAvailableErrMsg(error.message)
setIsAllSlotLoading(false)
})
}
useEffect(() => {
if (user !== null) {
getJoinedList()
}
}, [user])
Here is the code for getAvailableSlot(), I am using Aws amplify, so it actually return a promise for the GET request
import { API } from 'aws-amplify';
export const getAllAvailableSlot = async () => {
let path2 = path + '/list_all'
return API.get(apiName, path2)
}
What I tried:
Put in getAvailableSlotList as a callback function of getJoinedList(), like this:
const getJoinedList = (callback) => {
getUserJoinedSlotList()
.then(res => {
setIsLoading(false)
setJoinedSlotList(res.joined_slot)
})
.catch(error => {
setIsLoading(false)
setErrorMsg(error.message)
})
callback()
}
then
useEffect(() => {
if (user !== null) {
getJoinedList(getAvailableSlotList) // put in as call back here
}
}, [user])
By this, getAllAvailableSlot is called, I getting the result. But the result is not being set after calling setAvailableSlotList, and setIsAllSlotLoading(false) is not working as well, still true
Then I tried to call like this:
const getJoinedList = () => {
getUserJoinedSlotList()
.then(res => {
setIsLoading(false)
setJoinedSlotList(res.joined_slot)
getAvailableSlotList() // here call the function
})
.catch(error => {
setIsLoading(false)
setErrorMsg(error.message)
})
}
Again, is same result as above attempt.
Then I tried like this as well:
const calling = async () => {
await getJoinedList()
await getAvailableSlotList() //setAvailableSlotList and setAllIsLoading is not working, the 2ND CALL
}
useEffect(() => {
if (user !== null) {
//getJoinedList()
calling()
}
}, [user])
But still the getAvailableSlotList() set hooks is not taking any effect.
Specific problem:
I noticed that, the 2nd API calling is successful, but the follow up function which I call to setting the hooks, it just not taking any effect.
Means that:
Everything in getJoinedList() is working just fine. But when reach to getAvailableSlotList(), I can get the API result from it, but the setAvailableSlotList and setIsAllSlotLoading both cant set in the value
Question:
How to call another API after 1 API call is finished?
How to set react hooks at the 2nd API call?
Your second attempt should work. Here is a simplified sandbox example: https://codesandbox.io/s/romantic-bhaskara-odw6i?file=/src/App.js
A bit explanation on where the first and third attempts went wrong:
The first attempt is almost there, just that you need to move callback() inside the .then() block which essentially brings you to the second attempt.
The third one you used async/await but the problem is neither getJoinedList() nor getAvailableSlotList() returns a Promise so both requests will be sent around the same time without one waiting on the other to resolve first.
The simplest solution is actually to add your entire getAllAvailableSlot() function inside the getUserJoinedSlotList() through chaining. I see you're already using that, so I don't need to explain it in depth.
getUserJoinedSlotList().then(res => {
--- logic ---
getAllAvailableSlot().then(res2 => {
-- logic ---
}
}
Then chaining and its pairing could work here.
await getUserJoinedSlotList()
.then(res => /*assign hooks data*/)
.then(() => getAllAvailableSlot())
.then(availableSlot => /*assign availableSlot data*/)
.catch(e => console.log(e))

Fetch res.json() Attempt to invoke intergace method 'java.lang.String...'

I'm trying to convert a response from fetch function into json format but when I do so I get an error Attempt to invoke interface method 'java.lang.string com.facebook.react.bridge.ReadableMap.getString(java.lang.String)' on a null object reference.
Here is my code snippet with fetch function:
export const fetchAllUsers = () => {
fetch('http://192.168.1.103:3000/api/userData')
.then(res => {
res.json();
//console.warn('res keys = ' + Object.keys(res))
})
}
If comment back the row with console.warn I see the following "res keys = type, status, ok, statusText, headers, url, _bodyInit, _bodyBlod, bodyUsed".
bodyUsed = false
status = 200
type = default
Why I can't convert a response into json format? Or is there any another way to do so?
UPDATE
I've added the second then but I still get the error and the console.warn('res is json') is not running:
export const fetchAllUsers = () => {
fetch('http://192.168.1.103:3000/api/userData')
.then(res => {
res.json();
//console.warn('res keys = ' + Object.keys(res));
})
.then(res => {
console.warn('res is json');
console.warn(res);
})
}
UPDATE_2
I've run fetch function with another url but still got the problem. It seems like .json() causes the error. When I'm trying to console the result of fetch in the first .then() I get json object with type, status etc keys.
export const fetchAllUsers = () => {
fetch(`http://${localIP}:${port}/api/userData`)
//.then(res => res.json())
.then(json => console.warn('JSON: ' + json))
.catch(e => console.warn('ERROR: ' + e))
}
UPDATE_3
Forgot to mention that I'm creating an Android app with React Native. For testing I'm using a physical smartphone. Chrome version there is 73.0.3683.
I've replaced my fetch query with the following:
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json));
But still get the same error.
When I run it in https://jsfiddle.net/ it works. So the reason is hidden inside the code execution on a smartphone.
There must be more context to your problem; see the below snippet. This clearly works.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json));

Is there a way to set state for each iteration of a foreach

I'm working with an API within a React Application and I'm trying to make the API calls come back as one promise.
I'm using the Promise.all() method which is working great.
I'm stuck trying to set the results of two API calls to state with their own name. The promise code is working correctly and I am trying to forEach() or map() over the two sets of data and save them to state with their own name.
I'm sure there is a simple solution but I've been scratching my head for far too long over this!
I've tried searching all the docs for .map and .forEach with no luck!
fetchData(){
this.setState({loading: true})
const urls = ['https://api.spacexdata.com/v3/launches/past', 'https://api.spacexdata.com/v3/launches']
let requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => {
return responses
})
.then(responses => Promise.all(responses.map(r => r.json())))
.then(launches => launches.forEach(obj => {
// I need to set both values to state here
}))
.then(() => this.setState({loading: false}))
}
The API call returns two different arrays. I need to set both arrays to State individually with their own name. Is this possible?
If I understand your question correctly, a better approach might be to avoid iteration altogether (ie the use of forEach(), etc). Instead, consider an approach based on "destructuring syntax", seeing you have a known/fixed number of items in the array that is resolved from the prior promise.
You can make use of this syntax in the following way:
/*
The destructing syntax here assigns the first and second element of
the input array to local variables 'responseFromFirstRequest'
and 'responseFromSecondRequest'
*/
.then(([responseFromFirstRequest, responseFromSecondRequest]) => {
// Set different parts of state based on individual responses
// Not suggesting you do this via two calls to setState() but
// am doing so to explicitly illustrate the solution
this.setState({ stateForFirstRequest : responseFromFirstRequest });
this.setState({ stateForSecondRequest : responseFromSecondRequest });
return responses
})
So, integrated into your existing logic it would look like this:
fetchData() {
this.setState({
loading: true
})
const urls = ['https://api.spacexdata.com/v3/launches/past', 'https://api.spacexdata.com/v3/launches']
const requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([responseFromFirstRequest, responseFromSecondRequest]) => {
this.setState({ stateForFirstRequest : responseFromFirstRequest });
this.setState({ stateForSecondRequest : responseFromSecondRequest });
return responses
})
.then(() => this.setState({
loading: false
}))
}
If the two arrays won't interfere with each other in the state, is there a problem with just calling setState in each iteration?
.then(launches => launches.forEach(obj => {
this.setState({ [obj.name]: obj });
}))
If you want to minimise the number of updates then you can create an Object from the two arrays and spread that into the state in one call:
.then(launches => this.setState({
...launches.reduce((obj, launch) => {
obj[launch.name] = launch
return obj
}, {})
}))
forEach also provides the index as the second parameter. Wouldn't something like this work?
launches.forEach((obj, idx) => {
if (idx === 0) {
this.setState('first name', obj);
} else if (idx === 1) {
this.setState('second name', obj);
}
})
Also, this portion literally does nothing..
.then(responses => {
return responses
})
and the Promise.all() here also does nothing.
.then(responses => Promise.all(responses.map(r => r.json())))
should be
.then(responses => responses.map(r => r.json()))

Chain React setState callbacks

I need to load three different json files in an ordered sequence and with a fetch (the reason is i'm using nextjs export and i need those files to be read dynamically, so I fetch them when needed and their content can change even after the export)
The first file contains data that is used to create the url for the second file and so on, so each fetch needs an actually updated state to be fetched,
ATM the solution i'm using, since the second and third files are dependent from the first and second respectively, is fetching the first file and setting some state with setState, then in the setState callback fetch the second file and set some other state and so on:
fetch(baseUrl).then(
response => response.json()
).then(
res => {
this.setState({
...
}, () => {
fetch(anotherUrl+dataFromUpdatedState).then(
response => response.json()
).then(
res => {
this.setState({
...
}, () => {
fetch(anotherUrl+dataFromUpdatedState).then(
response => response.json()
).then(
res => {
this.setState({
})
}
)
})
}
).catch(
error => {
//error handling
}
)
})
}
).catch(
error => {
this.setState({ //an error occured, fallback to default
market: defaultMarket,
language: defaultLanguage,
questions: defaultQuestions
})
//this.setLanguage();
}
)
Now: I know that setState must be used carefully as it is async, but as far as I know the callback function is called after state is updated so from that point of view the state should update correctly. Is this solution anti-pattern, bad practice or should be avoided for some reason?
The code actually works, but i'm not sure if this is the way to do it.
You don't need to use the setState callback and read it from the state, since you can just read the data directly from the res object. This way you can make a flat promise chain.
Example
fetch(baseUrl)
.then(response => response.json())
.then(res => {
this.setState({
// ...
});
return fetch(anotherUrl + dataFromRes);
})
.then(response => response.json())
.then(res => {
this.setState({
// ...
});
return fetch(anotherUrl + dataFromRes);
})
.then(response => response.json())
.then(res => {
this.setState({
// ...
});
})
.catch(error => {
this.setState({
market: defaultMarket,
language: defaultLanguage,
questions: defaultQuestions
});
});

Categories

Resources