0

I'm working on learning NextJS. There might be an easy fix I'm not seeing so I appreciate if you all would take a look.

I have an API queryData(id) which will return data based on id.

profile will contain the user's profile which is a list of IDs.

subs.js

export default function Subs() {
  const { profile } = useProfileContext()
  const [userSubs, setUserSubs] = useState()

  useEffect(() => {
    const subArray = []
    profile.subIdList.map(subId => {
    
      console.log("ID: ", subId)
    
      queryData(subId.trim()).then(data => {
        subArray.push(data)
        return data
      })
    })

    console.log("USE EFFECT: ", subArray)  // Everything is as expected: List of JSON objects with metadata corresponding to ID.  
    setUserSubs(subArray)
  }, [])

  return (
     <>
       top
       {
         userSubs?.map(x => {
           return <p>hello {x}</p>  // Same result with or without return 
         })
       }
       end
     </>
  )
}

I expect to see something like:

top
hello data1
hello data2
hello data3
end

But the actual result is:

topend

So it's clear the data from the API is successful in being written into the state but the contents are not rendering properly from what I suspect the the map function isn't being rendered again after the userState is updated.

I've tried with replacing the default values for state with [1, 2, 3] and the setState([4, 5, 6]) and the render will be 4, 5, 6 so the issue is not the structure of the states but this does use return <p>...</p> in the map.

engineer-x
  • 2,173
  • 2
  • 12
  • 25
  • Adding return to the map has the same result with my dataset. Though it works if I'm using the [4, 5, 6] in the setState(). – engineer-x Nov 07 '22 at 02:57
  • But I understand you need return for curly brackets: https://stackoverflow.com/a/52019953/8278075 – engineer-x Nov 07 '22 at 02:59

1 Answers1

3

There's a race condition in your useEffect. You should wait until all promises are resolved:

  useEffect(() => {
    const subArray = []
    Promise.all(profile.subIdList.map(subId => { // added Promise.all( here
    
      console.log("ID: ", subId)
    
      return queryData(subId.trim()).then(data => {  // added return here
        subArray.push(data)
        return data
      })
    })).then(() => {                   // added .then() and {} wrapping the setUserSubs
      setUserSubs(subArray)
    })
  }, [])

Or, better yet, drop the subArray mutable/temp variable:

  useEffect(() => {
    Promise.all(profile.subIdList.map(subId => {
      return queryData(subId.trim()).then(data => {
        return data
      })
    })).then((subArray) => {
      setUserSubs(subArray)
    })
  }, [])

Demo below:

const { useState, useEffect } = React;
const useProfileContext = () => ({ profile: { subIdList: ['111', '222'] } });
const queryData = (s) => Promise.resolve(`data for ${s}`);

function Subs() {
  const { profile } = useProfileContext()
  const [userSubs, setUserSubs] = useState()

  useEffect(() => {
    Promise.all(profile.subIdList.map(subId => {
      return queryData(subId.trim()).then(data => {
        return data
      })
    })).then((subArray) => {
     console.log(subArray);
      setUserSubs(subArray)
    });
  }, [])

  return (
     <div>
       top
       {
         (userSubs || []).map(x => {
           return <p>hello {x}</p>
         })
       }
       end
     </div>
  )
}

ReactDOM.createRoot(document.getElementById('app')).render(<Subs />);
<script type="text/javascript" src="//unpkg.com/react@18/umd/react.production.min.js"></script>
<script type="text/javascript" src="//unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

<div id="app"></div>
acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • Yay, this works! Thank you. When you said there was a race condition, was the cause the usage of two async functions? I thought I was using them to wait for the function above it? – engineer-x Nov 07 '22 at 03:45
  • Your only async function is `queryData()`. It is a race condition because you call `queryData` several times, but don't await their completion before calling `setUserSubs`. `Promise.all()` takes an array of `Promise`s and "awaits" for the completion of all `Promise`s in such array. – acdcjunior Nov 08 '22 at 02:13
  • Ah! Thanks for making it clear! – engineer-x Nov 08 '22 at 06:20