This question title is potentially misleading, because by all accounts this should be working. All of my console.logs are popping up in the correct order, but my data does NOT populate correctly.
The premise of this is somewhat complicated. The app is essentially a video viewer, and you can get bronze, silver, or gold medals for completing a video. The page I'm currently working on is the display for these medal, and everything works perfectly IF I'm on the page and I reload it. If I navigate to the page for the first time, I get NO data in my local states, even though the console.log
before the line that sets the state fires before the line that uses the state. The issue is, by the time the state is used, it's magically cleared again, and there is no data available. My query looks like this...
// Grabs Medals
async function getChildsMedals(){
return await client.query({
query: GET_CHILD_VIDEO_STATISTICS,
fetchPolicy: 'network-only',
variables: {
childID: selectedChild.id
}
}).then( async (resolved) => {
console.log("IN THE ASYNC")
await setMedalData(resolved.data.getChildVideoStatistics.allTimeStats.individualVideoDetailedStats)
return (resolved.data.getChildVideoStatistics.allTimeStats.individualVideoDetailedStats)
}).catch(err => console.log(err))
}
Note that I added the line starting with await
right above the final return recently, as a double measure to assure the state gets set. It does not.
Also take note of the console.log
, this one fires before any others, which should indicate that what's getting done here should be FIRST.
Now for the states, I have two states that begin identically to each other, medals
and medalData
which both are initialized to this...
const [medalData, setMedalData] = useState({
step_up: {},
toe_walking: {},
toe_touches: {},
squat: {},
side_to_side: {},
rolling: {},
leg_lifts: {},
hands_to_knees: {},
chair_elevation: {},
floor_to_stand: {},
beam_balancing: {},
jump_rope: {},
jumping_jacks: {},
jump_forward_and_backward: {},
hop_scotch: {},
bear_crawl: {}
})
There is another local state, selectedChild
which tracks which child you are attempting to view the medals of. This can be set in several different ways, but this is not an issue since the selectedChild
value is always as anticipated.
This is set right at the start of the rendering, and I have two useEffects (definitely redundant, but one wasn't working so I tried another) to trigger the populate of the two aforementioned medal states,
// Populates earned medals
useEffect(() => {
getChildsMedals()
.then( (resolved) => {
console.log("AFTER THE ASYNC")
asyncSetMedalData(resolved)
})
setLoading(false)
}, [selectedChild])
// Populates earned medals unless async wants to keep being a fucking turd
useEffect(() => {
getChildsMedals()
.then( (resolved) => {
console.log("AFTER THE ASYNC")
asyncSetMedalData(resolved)
})
setLoading(false)
}, [])
The console logs when I first get to the page are as follows...
leg_lifts
LOG {"beam_balancing": {}, "bear_crawl": {}, "chair_elevation": {}, "floor_to_stand": {}, "hands_to_knees": {}, "hop_scotch": {}, "jump_forward_and_backward": {}, "jump_rope": {}, "jumping_jacks": {}, "leg_lifts": {}, "rolling": {}, "side_to_side": {}, "squat": {}, "step_up": {}, "toe_touches": {}, "toe_walking": {}}
LOG {}
LOG undefined
But when I save in my editor, which rerenders the page, I get the Expected print outs of...
LOG leg_lifts
LOG {"leg_lifts": {"bronze": 2, "gold": 0, "silver": 1}}
LOG {"bronze": 2, "gold": 0, "silver": 1}
LOG 0
What can I do to make this work in order? As you see in my useEffects, I even have a loading
state which prevents the screen from rendering prematurely, yet the issue persists
CodePudding user response:
On the direct question, if setLoading
should be done after asyncSetMedalData
(and that function is async), then do it either in the then
block (older syntax) or after awaiting it.
Here's what that looks like in either the old or new syntactic style (definitely choose one and stick with it).
Using newer async/await style:
useEffect(async () => {
const data = await getChildsMedals();
await asyncSetMedalData(data);
setLoading(false); // important: this follows an await
}, [selectedChild])
... or with the older style:
useEffect(() => {
getChildsMedals().then(data => {
asyncSetMedalData(data);
}).then(() => {
setLoading(false); // important: do this in a then block
});
}, [selectedChild])
For style completeness, here's the query method re-written the alternative styles:
With async/await:
async function getChildsMedals(){
try {
let data = await client.query({
query: GET_CHILD_VIDEO_STATISTICS,
fetchPolicy: 'network-only',
variables: { childID: selectedChild.id }
});
const stats = data.data.getChildVideoStatistics.allTimeStats.individualVideoDetailedStats;
await setMedalData(stats);
return stats;
} catch (err) {
console.log(err);
}
}
...or with the older style:
function getChildsMedals(){
return client.query({
query: GET_CHILD_VIDEO_STATISTICS,
fetchPolicy: 'network-only',
variables: { childID: selectedChild.id }
}).then(data => {
const stats = data.data.getChildVideoStatistics.allTimeStats.individualVideoDetailedStats;
return setMedalData(stats).then(() => stats);
}).catch(err => console.log(err))
}
CodePudding user response:
The solution I used is probably a little overcomplicated but it got the work done. I added two local states, noContent
and tryAgain
.
noContent
works almost like loading
states would, but after the query a conditional is run to see if any of the states or query results have any undefined
or null
values where it shouldn't. If it does, noContent
remains true despite the query 'ending' and loading
being set back to false. When this happens, tryAgain
changes its value to fire a useEffect (the value just alternates to true or false)
Considering this, my useEffects look like this now
///////////////////////////
/// ///
/// UseEffects ///
/// ///
///////////////////////////
// Populates earned medals
useEffect(() => {
// sets just medals (Object of objects)
getChildsMedals() // sets medalData
}, [selectedChild])
// Attempts rerender is somehow resolved is undefined
useEffect(() => {
getChildsMedals() // sets medalData
}, [tryAgain])
// Sets medal count state (object of ints)
useEffect(() => {
asyncSetMedals() //previously asyncSetMedalsData
.then(() => {
setLoading(false)
})
}, [medalData])
and the two functions these useEffects use look like this...
//////////////////////////////////////////////////////////////////////
// Sets the medals state with the medal count ({bronze: 1, silver: 2, gold: 3})
async function asyncSetMedals(){
let unlocked = (Object.keys(medalData))
unlocked.forEach( async (vidMedals) => {
await setMedals({...medals, [vidMedals]: {...medalData[vidMedals]}})
})
setNoContent(false)
}
//////////////////
// Grabs Medals //
async function getChildsMedals(){
// QUERY
return await client.query({
query: GET_CHILD_VIDEO_STATISTICS,
fetchPolicy: 'network-only',
variables: {
childID: selectedChild.id
}
}).then( (resolved) => {
setMedalData(resolved.data.getChildVideoStatistics.allTimeStats.individualVideoDetailedStats)
return resolved
}).catch(err => console.log(err))
}