简体   繁体   中英

Issues rendering data fetched with useEffect in a custom hook

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])
  • Another thing to note here is that 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 )
  • Also while updating locations you are overriding the locations, please use object for 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:

  1. 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 .

  2. 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
  3. 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.

  4. I don't use axios , but Shubham Khatri points out issues with the usage of it in your code.

  5. 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.

  6. 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM