I am a beginner in React and using React hooks. I have a function component that needs to show some info on the page. I receive the and return the jobs
array correctly and I can iterate in it with map
and show its info successfully, but want to receive another array too: location
. this array is based on the jobs
array: I need to receive jobs
first, then get its location id and then receive info and put it into my location array to be used in my info page later. the problem is, that when I add the location array I receive multiple errors such as res.json is not a function
and so on. here's my code:
function useJobs () {
const [jobs, setJobs] = React.useState([])
const [locations, setLocations] = React.useState([])
React.useEffect(() => {
fetch('/api/jobs/list-jobs', { headers: headers })
.then(r => r.json())
.then(setJobs)
}, [])
React.useEffect(() => {
jobs.map(job => (
axios.get(`/api/jobs/view-location/${job.location}/`, { headers: headers })
.then(res => res.json())
.then(setLocations)
))
}, [jobs], [])
return (jobs, locations)
}
export default function Jobs () {
const classes = useStyles()
const jobs = useJobs()
const locations = useJobs()
return (
<>
{jobs.map(job => (
<>
<div className={classes.root} key={job.id}>
......
<Row>
<Col style={{ color: 'black' }}>Title:{job.title} </Col>
<Col>Company Name:{job.company_name} </Col>
<Col style={{ color: 'black' }}>Internal Code:{job.internal_code} </Col>
</Row>
{locations.map(location => (
<Col key={location.id} style={{ color: 'black' }}>Location:{location.country}</Col>))
}
as you see, before I added the location
part, my job
info was showing correctly. but when I want to show the location info based on the info that job.location
GET request provided, I receive these errors. what part am I doing wrong? what is the correct way to implement this?
An axios request response doens't need to be run through res.json()
. It is required for a fetch request. Also axios
response has multiple information and the data is provided with resp.data
Also useEffect dependency is just one argument instead you are passing two
React.useEffect(() => {
axios('/api/jobs/list-jobs', { headers: headers })
.then(res => setJobs(res.data))
}, [])
React.useEffect(() => {
jobs.map(job => (
axios.get(`/api/jobs/view-location/${job.location}/`, { headers: headers })
.then(res => setLocations(prev => ({...prev, [job.id]: res.data})))
))
}, [jobs])
useJobs
hook returns both locations and jobs so you don't need to execute it twice and not that you need to return the result from useJobs as an object ie have return { jobs, locations }
instead of
return ( jobs, locations )
Full code:
function useJobs () {
const [jobs, setJobs] = React.useState([])
const [locations, setLocations] = React.useState({})
React.useEffect(() => {
axios('/api/jobs/list-jobs', { headers: headers })
.then(res => setJobs(res.data))
}, [])
React.useEffect(() => {
for (const job of jobs) {
axios(`/api/jobs/view-location/${job.location}/`, { headers: headers })
.then((data) => {
setLocations(prev => ({...prev, [job.id]: res.data}))
})
}
}, [jobs])
return [jobs, locations]
}
export default function Jobs () {
const classes = useStyles()
const { jobs,locations} = useJobs();
return (
<>
{jobs.map(job => (
<>
<div className={classes.root} key={job.id}>
......
<Row>
<Col style={{ color: 'black' }}>Title:{job.title} </Col>
<Col>Company Name:{job.company_name} </Col>
<Col style={{ color: 'black' }}>Internal Code:{job.internal_code} </Col>
</Row>
{locations[job.id].map(location => (
<Col key={location.id} style={{ color: 'black'
}}>Location:{location.country}</Col>))
}
This line does not return jobs
and locations
:
return (jobs, locations)
Instead, it evaluates jobs
, throws away that result, evaluates locations
, and returns that value.
If you meant to return an array containing jobs
and locations
, you can either return an array containing them as entries:
return [jobs, locations];
and use it like this, rather than making two calls to useJobs
:
const [jobs, locations] = useJobs();
or return an object containing them as properties:
return {jobs, locations};
and use it like this, again rather than making two calls to useJobs
:
const {jobs, locations} = useJobs();
There are a couple of other things that jump out:
As I mentioned in a comment, your code is falling prey to the footgun in the fetch
API: fetch
only rejects on network error, not HTTP error; you have to check for that separately (usually by checking response.ok
). Details on my anemic little blog .
useEffect
only accepts up to two arguments, not three. You're using three here:
React.useEffect(() => { jobs.map(job => ( axios.get(`/api/jobs/view-location/${job.location}/`, { headers: headers }).then(res => res.json()).then(setLocations) )) }, [jobs], []) // −−−−−−^^^^−−−−−−−−−−−−−−−− remove
It seems odd to use fetch
in one place but axios
in another. I suggest standardizing on one or the other. Unless you have a good reason for using axios
, I'd probably just use a wrapper on fetch
that checks for HTTP errors and rejects.
I don't use axios
, but Shubham Khatri points out issues with the usage of it in your code.
You're using map
as a simple iterator for an array. That's never appropriate, map
is for creating a new array from the results of the map
callback. If you're not using the return value from map
, use forEach
(or for-of
) instead.
You're doing an ajax call per job in a loop, but then storing the results in locations
. That means the results from each call in the loop overwrite any previous results. Presumably you meant to do something with all of the results.
Taking that all together:
// A reusable JSON fetch wrapper that handles HTTP errors
function fetchJSON(...args) {
return fetch('/api/jobs/list-jobs', { headers: headers })
.then(r => {
if (!r.ok) {
throw new Error("HTTP error " + r.status)
}
return r.json()
});
}
function useJobs () {
const [jobs, setJobs] = React.useState([])
const [locations, setLocations] = React.useState([])
React.useEffect(() => {
fetchJSON('/api/jobs/list-jobs', { headers: headers })
.then(setJobs)
}, [])
React.useEffect(() => {
// *** Don't use `map` as a simple iteration construct
for (const job of jobs) {
fetchJSON(`/api/jobs/view-location/${job.location}/`, { headers: headers })
.then(setLocations) // *** Need to handle #6 here
}
}, [jobs]) // *** No additional array here
return [jobs, locations]
}
export default function Jobs () {
const classes = useStyles()
const [jobs, locations] = useJobs() // *** Use jobs and locations here
return (
<>
{jobs.map(job => (
<>
<div className={classes.root} key={job.id}>
......
<Row>
<Col style={{ color: 'black' }}>Title:{job.title} </Col>
<Col>Company Name:{job.company_name} </Col>
<Col style={{ color: 'black' }}>Internal Code:{job.internal_code} </Col>
</Row>
{locations.map(location => (
<Col key={location.id} style={{ color: 'black' }}>Location:{location.country}</Col>))
}
...but that doesn't handle #6.
In addition to the other answers you have also have a problem calling setLocations
.
Because you call it from inside the map function, you are calling it once for every job.
jobs.map(job => (
axios.get(`/api/jobs/view-location/${job.location}/`, { headers: headers })
.then(setLocations)
))
You are effectively saying:
setLocations('London');
setLocations('Paris');
It is clear that if you call this twice you loose London, and only persist the final location.
Instead you meant to store all the locations in an array, so wait for all the promises to finish then store them all together.
Promise.all(
jobs.map(job => (
axios.get(`/api/jobs/view-location/${job.location}/`, { headers: headers })
.then(response => response.data)
))
)
.then(allLocations => setLocations(allLocations))
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.