Home > Blockchain >  Async Is Failing Me-- Component populates states before query finishes
Async Is Failing Me-- Component populates states before query finishes

Time:09-07

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))
    }

  • Related