Jackal
Jackal

Reputation: 3521

React how to wait until state is updated without extra state variables?

I cannot understand. To cut it short, I have these 2 variables which are being used in the state

questionLoaded
gameOver

This component needs to wait until it finishes getting data using the function selectRandomQuestion.

Therefore if i remove the questionLoaded, it will give me exception when it's looping through the map.

Now I had to create another variable to check when the game is over to unrender this component, therefore the terniary condition questionLoaded || gameOver ?

I tried checking the variable currentQuestion to conditionally render, but for some reason it will give exception when looping through the map in the options array of this property. Which means currentQuestion gets data first before it's own child property options has any data when hitting the condition check

The problem is that this looks kind of dirty, I'm still using react just for a few weeks and I'm not sure if this is the appropriate way to deal with state updates or is there a better way to handle it.

To sum it up, my problem is when rendering components, I often need to wait for the state to update when the rendering depends on some conditions, and everytime I need a new condition that means I need another set of logic using another state variable. Which means creating another useEffect with extra logic and to be triggered when that variable is updated.

import React, { useState, useEffect } from 'react';
import './GuessPicture.css';
import questions from './data/questions.js';

function GuessPicture() {
    const [currentQuestion, setCurrentQuestion] = useState({});
    const [unansweredQuestions, setUnansweredQuestions] = useState([]);
    const [answeredQuestions, setAnsweredQuestions] = useState([]);
    const [questionLoaded, setQuestionLoaded] = useState(false);
    const [gameOver, setGameOver] = useState(false);

    useEffect(() => {
        setUnansweredQuestions(questions);
        selectRandomQuestion();
        setQuestionLoaded(true);
    }, []);

    useEffect(() => {
        if (unansweredQuestions.length > 0) nextQuestion();
        else alert('game over');
    }, [unansweredQuestions]);

    function selectRandomQuestion() {
        const index = Math.floor(Math.random() * questions.length);
        let selectedQuestion = questions[index];
        selectedQuestion.options = shuffle(selectedQuestion.options);
        setCurrentQuestion(selectedQuestion);
    }

    function nextQuestion() {
        const index = Math.floor(Math.random() * unansweredQuestions.length);
        let selectedQuestion = unansweredQuestions[index];
        selectedQuestion.options = shuffle(selectedQuestion.options);
        setCurrentQuestion(selectedQuestion);
    }

    // Fisher Yates Shuffle Algorithm
    function shuffle(array) {
        var currentIndex = array.length,
            temporaryValue,
            randomIndex;

        // While there remain elements to shuffle...
        while (0 !== currentIndex) {
            // Pick a remaining element...
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex -= 1;

            // And swap it with the current element.
            temporaryValue = array[currentIndex];
            array[currentIndex] = array[randomIndex];
            array[randomIndex] = temporaryValue;
        }

        return array;
    }

    const onClickOption = (event) => {
        if (currentQuestion.correctValue == event.target.dataset.value) {
            setAnsweredQuestions((answeredQuestions) => [
                ...answeredQuestions,
                currentQuestion,
            ]);

            const newUnansweredQuestions = unansweredQuestions.filter(
                (item) => item.id != currentQuestion.id
            );

            setUnansweredQuestions(newUnansweredQuestions);
        } else alert('Wrong');
    };

    return (
        <>
            {questionLoaded || gameOver ? (
                <div className="guess-picture-container">
                    <div className="guess-picture">
                        <img src={currentQuestion.image} alt="English 4 Fun" />
                    </div>
                    <div className="guess-picture-answers-grid">
                        {currentQuestion.options.map((key) => (
                            <button
                                key={key.value}
                                onClick={onClickOption}
                                data-value={key.value}
                            >
                                {key.display}
                            </button>
                        ))}
                    </div>
                </div>
            ) : null}
        </>
    );
}

export default GuessPicture;

Upvotes: 1

Views: 278

Answers (1)

Glen Carpenter
Glen Carpenter

Reputation: 412

Your code looks fine to me, however I would avoid the ternary operator in this situation and use the short-circuit operator instead. This makes your code a little cleaner.

instead of:

{questionLoaded || gameOver ? <SomeComponentHere /> : null}

try using:

{(questionLoaded || gameOver) && <SomeComponentHere />}

Why avoid the ternary operator? Because it's not a ternary operation, it is a boolean check. This makes your code more semantically appropriate.

Upvotes: 2

Related Questions