Client: React, mobx Server: NodeJS, MongoDB
Short question:
I have an array of elements which fills inside of useEffect function, expected result: each element of array should be rendered, actual result: nothing happens. Render appears only after code changing in VSCode.
Tried: changing .map to .forEach, different variations of spread operator in setState(...[arr]) or even without spread operator, nothing changes.
Info:
Friends.jsx part, contains array state and everything that connected with it, also the fill-up function.
const [requestsFrom, setRequestsFrom] = useState([]) //contains id's (strings) of users that will be found in MongoDB
const [displayRequestsFrom, setDisplayRequestsFrom] = useState([]) //should be filled by elements according to requestsFrom, see below
const getUsersToDisplayInFriendRequestsFrom = () => {
const _arr = [...displayRequestsFrom]
requestsFrom.map(async(f) => {
if (requestsFrom.length === 0) {
console.log(`empty`) //this part of code never executes
return
} else {
const _candidate = await userPage.fetchUserDataLite(f)
_arr.push( //template to render UserModels (below)
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
console.log(_arr)
}
})
setDisplayRequestsFrom(_arr)
// console.log(`displayRequestsFrom:`)
console.log(displayRequestsFrom) //at first 0, turns into 3 in the second moment (whole component renders twice, yes)
}
Render template function:
const render = {
requests: () => {
return (
displayRequestsFrom.map((friendCandidate) => {
return (
<FriendModel link={friendCandidate.link} username={friendCandidate.username} userId={friendCandidate.userId}/>
)
})
)
}
}
useEffect:
useEffect(() => {
console.log(`requestsFrom.length === ${requestsFrom.length}`)
if (!requestsFrom.length === 0) {
return
} else if (requestsFrom.length === 0) {
setRequestsFrom(toJS(friend.requests.from))
if (toJS(friend.requests.from).length === 0) {
const _arr = [...requestsFrom]
_arr.push('0')
setRequestsFrom(_arr)
}
}
if (displayRequestsFrom.length < 1 && requestsFrom.length > 0) {
getUsersToDisplayInFriendRequestsFrom()
//displayRequestsFrom and requestsFrom lengths should be same
}
},
[requestsFrom]
)
Part of jsx with rendering:
<div className={styles.Friends}>
<div className={styles['friends-container']}>
{render.requests()}
</div>
</div>
UPD: my console.log outputs in the right order from beginning:
requestsFrom.length === 0
requestsFrom.length === 3
displayRequestsFrom === 0
displayRequestsFrom === 3
As we can see, nor requestsFrom, neither displayRequestsFrom are empty at the end of the component mounting and rendering, the only problem left I can't find out - why even with 3 templates in displayRequestsFrom component doesn't render them, but render if I press forceUpdate button (created it for debug purposes, here it is:)
const [ignored, forceUpdate] = React.useReducer(x => x + 1, 0);
<button onClick={forceUpdate}>force update</button>
PRICIPAL ANSWER
The problem here is that you are executing fetch inside .map method. This way, you are not waiting for the fetch to finish (see comments)
Wrong Example (with clarification comments)
const getUsersToDisplayInFriendRequestsFrom = () => {
const _arr = [...displayRequestsFrom];
// we are not awating requestsFrom.map() (and we can't as in this example, cause .map is not async and don't return a Promise)
requestsFrom.map(async (f) => {
const _candidate = await userPage.fetchUserDataLite(f)
// This is called after setting the state in the final line :(
_arr.push(
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
} )
setDisplayRequestsFrom(_arr) // This line is called before the first fetch resolves.
// The _arr var is still empty at the time of execution of the setter
}
To solve, you need to await for each fetch before updating the state with the new array.
To do this, your entire function has to be async and you need to await inside a for loop.
For example this code became
const getUsersToDisplayInFriendRequestsFrom = async () => { // Note the async keyword here
const _arr = [...displayRequestsFrom]
for (let f of requestsFrom) {
const _candidate = await fetchUserData(f)
_arr.push(
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
}
setDisplayRequestsFrom(_arr)
}
You can also execute every fetch in parallel like this
const getUsersToDisplayInFriendRequestsFrom = async () => { // Note the async keyword here
const _arr = [...displayRequestsFrom]
await Promise.all(requestsFrom.map((f) => {
return fetchUserData(f).then(_candidate => {
_arr.push(
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
});
}));
setDisplayRequestsFrom(_arr);
}
Seems you are mapping on an empty array where you are trying to call your service.
const getUsersToDisplayInFriendRequestsFrom = () => {
const _arr = [...displayRequestsFrom]
/* HERE */ requestsFrom.map(async(f) => {
if (requestsFrom.length === 0) {
return
If the array (requestsFrom) is empty ( as you initialized in the useState([]) ) the function you pass in the map method is never called.
Not sure what you are exactly trying to do, but this should be one of the problems...
Also, you shoudn't use state to store rendered components
_arr.push(
<FriendModel key={_candidate.id} isRequest={true} link='#' username={_candidate.login} userId={_candidate._id}/>
)
, instead you should map the data in the template and then render a component for each element in your data-array.
For example:
function MyComponent() {
const [myData, setMyData] = useState([{name: 'a'}, {name: 'b'}])
return (<>
{
myData.map(obj => <Friend friend={obj} />)
}
</>)
}
Not:
function MyComponent() {
const [myDataDisplay, setMyDataDisplay] = useState([
<Friend friend={{name: 'a'}} />,
<Friend friend={{name: 'b'}} />
])
return <>{myDataDisplay}</>
}
useEffect
to initialize your stateI'm wondering why you are setting the requestsFrom
value inside the useEffect
.
Why aren't you initializing the state of your requestsFrom
inside the useState()
?
Something like
const [requestsFrom, setRequestsFrom] = useState(toJS(friend.requests.from))
instead of checking the length inside the useEffect
and fill it
So that your useEffect can became something like this
useEffect(() => {
if (displayRequestsFrom.length < 1 && requestsFrom.length > 0) {
getUsersToDisplayInFriendRequestsFrom()
}
},
[requestsFrom]
)