0

Currently i'm doing a quiz composed by multiple categories that can be chosen by the user and i wanna check if the user responded to all questions. For doing that, i compared the number of questions he answered with the number of questions gived by the api response. The problem is that i have an "submit answers" button at the end of the last question, with that onClick function:

const sendAnswers = (e, currentQuiz) => {
        setQuizzes({...quizzes, [currentQuiz]:answers});
        setAnswers([])


        var answeredToAllQuestions = true
        
        DataState.map(function (quiz) {
            if(quiz.category in quizzes){
                if(Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
                    answeredToAllQuestions=false;
                }
            }
        }); 

        if(answeredToAllQuestions === false){
            setAlertTrigger(1);
        }
        else{
            setNumber(number+1);
        }
    
}

in that function i use setState on this line: setQuizzes({...quizzes, [currentQuiz]:answers}); to upload the answers he checked on the last question before checking if he answered to all questions. The problem is that state of quizzes is not updated imediatly and it s not seen by the if condition.

I really don't know how am i supposed to update the state right after setting it because, as i know, react useState updates the state at the next re-render and that causes trouble to me..

Mihai Ene
  • 11
  • 4
  • 1
    Does this answer your question? [Why is setState in reactjs Async instead of Sync?](https://stackoverflow.com/questions/36085726/why-is-setstate-in-reactjs-async-instead-of-sync) – Sinan Yaman Aug 10 '21 at 12:44
  • No, because literally everyone tells me "setState is Async" and i already know that..I tried to modify my code like this: ```setQuizzes({...quizzes, [currentQuiz]:answers}, () => #CHECKING THINGS )); ``` to check right after the state is updated but it won t work..:( – Mihai Ene Aug 10 '21 at 12:50
  • 1
    @MihaiEne [This](https://stackoverflow.com/a/58877875/697154) is the answer you should be reading. As you stated yourself, it's completely misguided to blame the *async* nature of `setState` for the observed behavior. Because incidentally it would be the same if `setState` were in fact *sync*. What you're experiencing is a [*closure problem*](https://stackoverflow.com/q/111102/697154), and you need to deal with that accordingly. – Yoshi Aug 10 '21 at 12:59
  • 1
    Also `useState`'s updater != `this.setState`. It does not accept a second parameter callback like `setState` does, so your function is just getting ignored. – Brian Thompson Aug 10 '21 at 13:13
  • A simple solution would be to store the new value in a variable first. `const newQuizzes = {...quizzes, [currentQuiz]:answers}` then `setQuizzes(newQuizzes)` then use `newQuizzes` instead of `quizzes`. – 3limin4t0r Aug 10 '21 at 13:37

2 Answers2

0

You have to either combine the useState hook with the useEffect or update your sendAnswers method to perform your control flow through an intermediary variable:

  • Using a temporary variable where next state is stored:

      const sendAnswers = (e, currentQuiz) => {
          const newQuizzes = {...quizzes, [currentQuiz]:answers};
    
          let answeredToAllQuestions = true
    
          DataState.map(function (quiz) {
              if(quiz.category in newQuizzes){
                  if (Object.keys(quiz.questions).length !== Object.keys(newQuizzes[quiz.category]).length){
                      answeredToAllQuestions = false;
                  }
              }
          }); 
    
          setQuizzes(newQuizzes);
          setAnswers([]);
          if (answeredToAllQuestions === false) {
              setAlertTrigger(1);
          } else {
              setNumber(number+1);
          }
    
    }
    
  • Using the useEffect hook:

      const sendAnswers = (e, currentQuiz) => {
          setQuizzes({...quizzes, [currentQuiz]:answers});
          setAnswers([]);
      }
    
      useEffect(() => {
          let answeredToAllQuestions = true
    
          DataState.map(function (quiz) {
              if(quiz.category in quizzes){
                  if (Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
                      answeredToAllQuestions = false;
                  }
              }
          });
    
          if (answeredToAllQuestions === false) {
              setAlertTrigger(1);
          } else {
              setNumber(number+1);
          }
      }, [quizzes]);
    
tmarwen
  • 15,750
  • 5
  • 43
  • 62
0

Considering that quizzes will be equal to {...quizzes, [currentQuiz]:answers} (after setQuizzes will set it), there is no reason to use quizzes in if condition. Replace it with a local var and problem will be solved.

const sendAnswers = (e, currentQuiz) => {
    let futureValueOfQuizzes = {...quizzes, [currentQuiz]:answers}
    setQuizzes(futureValueOfQuizzes);
    setAnswers([])


    var answeredToAllQuestions = true
    
    DataState.map(function (quiz) {
        if(quiz.category in futureValueOfQuizzes){
            if(Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
                answeredToAllQuestions=false;
            }
        }
    }); 

    if(answeredToAllQuestions === false){
        setAlertTrigger(1);
    }
    else{
        setNumber(number+1);
    }

}

I would like to take this opportunity to say that these type of problems appear when you use React state for your BI logic. Don't do that! Much better use a local var defined in components body:

const Component = () => {
   const [myVar , setMyVar] = useState();
   let myVar = 0;   
   ...           
}

If myVar is used only for BI logic, use the second initialization, never the first!

Of course sometimes you need a var that is in BI logic and in render (so the state is the only way). In that case set the state properly but for script logic use a local var.

Giovanni Esposito
  • 10,696
  • 1
  • 14
  • 30