so I'm rendering out an array of posts. Each post has a unique user id associated with it, which I'm trying to use in another API call to return the user's image for that post.
Currently i have it setup like so
Rendering out the post
{data?.map((data) => (
<div className="allpostsingle" key={data.id} onClick={() => sendTo(data.id)}>
<div className="allpostheader">
<img src={getUrl(data.user)}/>
<p>{data?.description}</p>
</div>
</div>
))}
The function to get the image URL
const getUrl = async (id) => {
let url = `http://127.0.0.1:8000/api/user/${id}/`
const response = await fetch(url)
const data = await response.json()
const avatarurl = data.avatar
return avatarurl
}
The issue with this is it returns a promise instead of the URL. I know you can set it in a state to get the url, but then it wouldnt be unique to each post?
Im just wondering how i can solve this or if theres a different way to solve this issue.
CodePudding user response:
You can't assign the return value of getUrl()
to the src
attribute because it is asynchronous and returns a Promise
.
What you can do instead is maintain another piece of state for the user avatars that updates when your data
does. For example, create a function that resolves user avatars
const resolveUserAvatars = async (users) => {
const avatars = await Promise.allSettled(users.map(async ({ user }) => {
const res = await fetch(`http://127.0.0.1:8000/api/user/${encodeURIComponent(user)}/`)
if (!res.ok) {
throw new Error(res.status)
}
return {
user,
avatar: (await res.json()).avatar
}
}))
return Object.fromEntries(
avatars
.filter(({ status }) => status === "fulfilled")
.map(({ value: { user, avatar } }) => [ user, avatar ])
)
)
}
Then in your component...
const [ data, setData ] = useState([]) // init as an array
const [ avatars, setAvatars ] = useState({})
// avatars will look like
// { userId: imageUrl, ... }
useEffect(() => {
resolveUserAvatars.then(setAvatars)
}, [ data ]) // execute when data changes
and in your JSX
{data.map(({ id, user, description }) => (
<div className="allpostsingle" key={id} onClick={() => sendTo(id)}>
<div className="allpostheader">
<img src={avatars[user] ?? "some/placeholder.png"}/>
<p>{description}</p>
</div>
</div>
))}
See also Promise.allSettled()
CodePudding user response:
It doesn't work because rendering in react in synchronous while your getUrl
is an async function. As some other answers pointed out, you can fetch all url in componentDidMount
or useEffect
hook and update the state.
If you don't want to change your existing code, you can also replace img
with a react component of your own so it can handle promise.
function Image({src}){
const [actualSrc, setActualSrc] = useState('placeholder image link')
useEffect(()=>{
src.then((url) => setActualSrc(url));
}, [src]);
return <img src={actualSrc}/>
}
{data?.map((data) => (
<div className="allpostsingle" key={data.id} onClick={() => sendTo(data.id)}>
<div className="allpostheader">
<Image src={getUrl(data.user)}/>
<p>{data?.description}</p>
</div>
</div>
))}
const getUrl = async (id) => {
let url = `http://127.0.0.1:8000/api/user/${id}/`
const response = await fetch(url)
const data = await response.json()
const avatarurl = data.avatar
return avatarurl
}
This may not be best for performance as getUrl
will send a new reference of promise on each render. In this case, best would be to pass id
(user id in getUrl
) to the Image
component instead of a promise and have logic of fetching the actualUrl inside of it
CodePudding user response:
It's not a good idea to get each user's avatar separately and send request for each post and every time render post items, If it's possible for you and the backend api just include users avatar url in post so easily render it. if its not possible and have a limitation in api you just need to get all post user data with Promiss.allSettled in useEffect after the post data has recieved and attach user data to postItem or keep it in separate dictionary then set the state to render the post and user avatar url in it.
CodePudding user response:
As you have said that your function returns a promise instead of URL (I had the same issue). This issue can happen to any asynchronous function when they are called as usual function (they will return a promise instead of an output), unless you use callback functions or async/await functions in a usual JavaScript code. But the in React, asynchronous functions should be called using UsEffect()
hook OR componentDidMount()
function. You need to refer to How to call an async function inside a UseEffect() in React? if your react component is a function. Or refer How to call an async function in componentDidMount function? if your react component is a class to know how exactly use them in react to handle asynchronous functions in React components.