0

I am having issues with (I think) the timing of the sequential calls to setState variables... specifically in my 'handleStartOverClick()' call. This function makes two 'setState' calls, builds a list, then selects a random item from that list. However, my console.logs in that method are showing that nothing is actually getting cleared, and the new list isn't getting built. I am pretty sure this isn't the best 'react way' to handle the synchronous nature of these calls, but I just haven't been able to find a way to properly handle from the docs/otherwise.

So far I've tried wrapping in useCallback(), making the methods async to use .then, and even brute forcing a timeout in between calls just to see it work. But no luck.

Also, I've included the json that is being pulled in with the import of 'questionsJson' at the top, even though that is maybe an extraneous detail.

import React, { useEffect, useMemo, useState } from 'react';
import { Grid } from '@material-ui/core';
import questionsJson from 'assets/data/questions.json';
import { Question } from 'domains/core/models';

export default function Questions(): ReactElement {
    const [leftOptionSelected, setLeftOptionSelected] = useState(false);
    const [rightOptionSelected, setRightOptionSelected] = useState(false);
    const [mainQuestionList, setMainQuestionList] = useState<Question[]>([]);
    const [currentQuestion, setCurrentQuestion] = useState<Question | undefined>(undefined);
    const [questionsLoaded, setQuestionsLoaded] = useState(false);

    const removeQuestionFromMainQuestionList = useMemo(() => (questionToRemove: Question) => {
        const updatedQuestionList = mainQuestionList.filter(question => question?.id !== questionToRemove?.id);
        setMainQuestionList(updatedQuestionList);
    },[setMainQuestionList, mainQuestionList]);

    const getRandomQuestion = useMemo(() => () => {
        for (let index = 0; index < mainQuestionList.length; index++) {
            const numberOfQuestions = mainQuestionList.length;
            const randomArrayPosition = Math.floor(Math.random() * numberOfQuestions);
            const randomQuestion = mainQuestionList[randomArrayPosition];
            
            setCurrentQuestion({
                id: randomQuestion.id,
                option1: randomQuestion.option1,
                option2: randomQuestion.option2
            });
            removeQuestionFromMainQuestionList(randomQuestion);
            console.log("new main question list", mainQuestionList)
        }
    },[mainQuestionList, removeQuestionFromMainQuestionList]);

    const buildQuestionList = () => {
        const questionList: Question[] = [];
        const questionInfo = questionsJson.questionInfo;
        for (let index = 0; index < questionInfo.length; index++) {
            const currentQuestionSet = questionInfo[index];
            let questions = currentQuestionSet.questions;
            
            const questionLimit = currentQuestionSet.questionLimit;
            for (let index = 0; index < questionLimit; index++) {
                const numberOfQuestions = questions.length;
                const randomArrayPosition = Math.floor(Math.random() * numberOfQuestions);

                const questionToAdd = questions[randomArrayPosition];
                questionList.push(questionToAdd);
                questions = questions.filter(question => question?.id !== questionToAdd?.id);
            }
        }
        setMainQuestionList(questionList);
        console.log("initial question list", questionList);
        setQuestionsLoaded(true);
    }

    useEffect(() => {
        buildQuestionList();
    }, []);
    
    useEffect(() => {
        if (questionsLoaded){
            getRandomQuestion();
        }
    }, [questionsLoaded]);

    const handleStartOverClick = () => {
        console.log("in handle start over")
        setCurrentQuestion(undefined);
        console.log("current question", currentQuestion);
        setMainQuestionList([]);
        console.log("mainQuestionList", mainQuestionList);
        buildQuestionList();
        console.log("mainQuestionList", mainQuestionList);
        
        setTimeout( () => { 
            getRandomQuestion();
            console.log("current question after clearout", currentQuestion);
        }, 4000);
        
    }

    const handleOptionClick = (sideClicked: string) => {
        if (sideClicked == "left"){
            setLeftOptionSelected(true);
            setTimeout( () => { 
                setLeftOptionSelected(false);
                getRandomQuestion();
            }, 2000);
        } else {
            setRightOptionSelected(true);
            setTimeout( () => {
                setRightOptionSelected(false)
                getRandomQuestion();
            }, 2000);
        }
    }
    return (
        <Grid container direction="row" spacing={6}>
            <Grid style={{ backgroundColor: "yellow"}} item xs={6}>
                <div onClick={() => handleOptionClick("left")} style={{ pointerEvents: rightOptionSelected ? 'none' : 'auto'}}>
                    <div>{currentQuestion?.option1}</div>
                </div>
            </Grid>

            <Grid style={{ backgroundColor: "green"}} item xs={6}>
                <div onClick={() => handleOptionClick("right")} style={{ pointerEvents: leftOptionSelected ? 'none' : 'auto'}}>
                    <span></span>
                    <div style={{ backgroundColor: "green"}}>{currentQuestion?.option2}</div>
                </div>
            </Grid>
            
            <Grid item xs={2}>
                <button onClick={() => handleStartOverClick()}>
                    START OVER
                </button>
            </Grid>
        </Grid>
    );
}
{
    "questionInfo":[
        {
            "category":"fun",
            "questionLimit":2,
            "questions":[
                {
                    "id":1,
                    "option1":"Apple pie",
                    "option2":"Cherry pie"
                },
                {
                    "id":2,
                    "option1":"Jack",
                    "option2":"Jill"
                }
            ]
        },
        {
            "category":"lifestyle",
            "questionLimit":2,
            "questions":[
                {
                    "id":3,
                    "option1":"Workout",
                    "option2":"Watch tv"
                },
                {
                    "id":4,
                    "option1":"Day",
                    "option2":"Night"
                },
                {
                    "id":5,
                    "option1":"Watch",
                    "option2":"Read"
                }
            ]
        },
        {
            "category":"interest",
            "questionLimit":2,
            "questions":[
                {
                    "id":6,
                    "option1":"Cars",
                    "option2":"Baseball"
                },
                {
                    "id":7,
                    "option1":"Money",
                    "option2":"Fame"
                }

            ]
        }
]
}
tbcodes33
  • 13
  • 6
  • The setState call are async in nature.This might help you potentially https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous – Rajesh Paudel Mar 04 '22 at 14:55

2 Answers2

0

Never set state and use the state value in the same function. Set state is async and will always console.log stale data in the same function.

simple solution:

const [state, setState] = useState({id: 0})

const someFunction = () => {
const newState = {id: 1}

setState(newState)
//console.log(state) NO! Stale data
console.log(newState) //New data. Use this instead of state inside this function
}

useEffect(() => {
 console.log(state) //New, updated value
}, [state]) //Only console log if new state is changed. 
SlothOverlord
  • 1,655
  • 1
  • 6
  • 16
0

From your implementation,

I can suggest you this way

Instead of using this

useEffect(() => {
   if (questionsLoaded){
      getRandomQuestion();
   }
}, [questionsLoaded]);

You should change it to this

useEffect(() => {
   if (mainQuestionList && mainQuestionList.length > 0){
      getRandomQuestion();
   }
}, [mainQuestionList]);

it means whenever you modify the question list, we will get the random question again.

After this change, you can get rid of questionsLoaded.

And in handleStartOverClick, you can remove the call of getRandomQuestion with setTimeout (buildQuestionList will help you to handle that case with useEffect after getting question list).

Nick Vu
  • 14,512
  • 4
  • 21
  • 31
  • that does look much cleaner, the only issue is that getRandomQuestion() itself alters mainQuestionList by removing the random question from the mainQuestionList so that it doesn't get selected again. So this causes a loop of rerenders till it gets to the end of the question list. If my implementation is incorrect, I'm very open to suggestions! – tbcodes33 Mar 04 '22 at 15:45
  • I'd suggest you to add this `mainQuestionList.filter(question => question?.id !== questionToRemove?.id)` directly to where you render the question list and avoid modifying the current `mainQuestionList`. That will help you to get rid of `removeQuestionFromMainQuestionList` too. – Nick Vu Mar 04 '22 at 16:06