-1

i get the following error when i try to load questions(Array) for a quiz from a db.json server.

TypeError: this.state.questions[this.state.currentQuestion] is undefined

here is a sandbox of my project (relevant classes are play_quiz.js and db.json https://codesandbox.io/s/kjllt?file=/quiz-project/server/db.json

the error occurs in line 79 of play_quiz.js

it worked just fine when i did it without the json server but not it seems to have some kind of problem recognising the content of the array. I hope someone can help me

nikva
  • 49
  • 5
  • 1
    The array is empty before the fetch finishes. so `this.state.questions[0]` returns undefined. See also: [Why calling react setState method doesn't mutate the state immediately?](https://stackoverflow.com/q/30782948/996081) - Check that the length is greater than 0. – cbr Jan 15 '21 at 00:26
  • but this.state.questions.length gives 3 as a result, so the array cant be empty – nikva Jan 15 '21 at 00:33
  • 1
    If you add `console.log('Length: ' + this.state.questions.length)` in `render()`, what do you see? – cbr Jan 15 '21 at 00:35
  • i tried it with setting showscore to true so it prints {this.state.questions.length}. the result was 3 – nikva Jan 15 '21 at 00:38
  • 1
    Put the `console.log()` in the render. The fetch may be fast enough locally that you just don't notice the change. – cbr Jan 15 '21 at 00:40
  • it says from database lenght 3 but then it says 0. Length: 0 play_quiz.js:62 Current state: play_quiz.js:15 From database: play_quiz.js:24 Array [] play_quiz.js:25 Length: 3 play_quiz.js:62 Length: 0 – nikva Jan 15 '21 at 00:46
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/227364/discussion-between-nikva-and-cbr). – nikva Jan 15 '21 at 00:52

1 Answers1

1

As discussed in the comments, the issue is that when the component renders for the first time, this.state.questions is an empty array as set in the state's initial state.

Because of this, this.state.questions[0] returns undefined, until the API response finishes (which is started by getRandomizedQuestions() which is called by componentDidMount after the component has been rendered and mounted).

You need to handle this case. The way to solve it depends on how you want to solve it and what you want to show to the user while it's loading. Here's a few solutions:

1. Just show "loading" until the array isn't empty.

render() {
  if (this.state.questions.length < 1) {
    return <div className="quiz-window">Loading...</div>;
  }

  return (
    <div className="quiz-window">
      // ...
    </div>
  );
}

2. Use another ternary to show "loading" in the place of the block of JSX which depends on the array:

render() {
  return (
    <div className="quiz-window">
      {this.state.showScore ? (
        <div className="score-section">
          korrekt beantwortet: {this.state.score} von{" "}
          {this.state.questions.length}
        </div>
      ) : this.state.questions.length > 0 ? (
        <>
          <div className="question-section">
            <div className="question-count">
              <span>Frage {this.state.currentQuestion + 1}</span>/
              {this.state.questions.length}
            </div>
            <div className="question-text">
              {this.state.questions[this.state.currentQuestion].title}
            </div>
          </div>
          <div className="answer-section">
            {this.state.questions[this.state.currentQuestion].answers.map(
              (answer) => (
                <button
                  onClick={() => this.handleAnswerOptionClick(answer.isCorrect)}
                >
                  {answer.title}
                </button>
              )
            )}
          </div>
        </>
      ) : (
        <p>Loading</p>
      )}
    </div>
  );
}

Alternatively, if it's possible that the API returns an empty array and you want to handle that as a separate case:

3. Introduce state variable that tracks when the data is loading and show loading until it's done

class Play_quiz extends React.Component {
  state = {
    currentQuestion: 0,
    showScore: false,
    score: 0,
    questions: [],
    isLoading: true,
  };

  // ...

  getRandomizedQuestions = () => {
    const apiUrl = "http://localhost:3001/questions";
    fetch(apiUrl)
      .then((response) => response.json())
      .then(
        (result) => {
          console.log("From database:");
          console.log(result);

          let amountOfQuestions = result.length;
          let randomizedResult = [];
          for (let i = 0; i < amountOfQuestions; i++) {
            let randomIndex = Math.floor(Math.random() * result.length);
            randomizedResult.push(result[randomIndex]);
            result.splice(randomIndex, 1);
          }

          this.setState({
            questions: randomizedResult,
            isLoading: false
          });
          // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        },
        (error) => {
          console.log("An unexpected error occurred", error);
        }
      );
  };

  // ...

  render() {
    if (this.state.isLoading) {
      return <div className="quiz-window">Loading...</div>;
    }

    return (
      <div className="quiz-window">
        // ...
      </div>
    );
  }
}

See also: Why calling react setState method doesn't mutate the state immediately?

cbr
  • 12,563
  • 3
  • 38
  • 63
  • wow, thanks for the effort with the detailed answer! unfortuinately there is still something not working in your solution number 2: TypeError: this.state.questions[this.state.currentQuestion].answers.map is not a function – nikva Jan 15 '21 at 01:04
  • weirdly enough its working sometimes until the first question but if it works it only works until i click one answer and i get the TypeError: this.state.questions[this.state.currentQuestion].answers.map is not a function – nikva Jan 15 '21 at 01:07
  • 1
    @nikva not all of your questions in `db.json` have an array as `answers` - some have a string. – cbr Jan 15 '21 at 01:13
  • yes, so i have to check if the question is multiple choice, if it isnt, then i have to completely redo how i load the questions? – nikva Jan 15 '21 at 01:19
  • 1
    Just change how you render them. The data loading aspect is already alright. If it's not an array (`Array.isArray()`), then don't `map` but render it somehow differently. – cbr Jan 15 '21 at 01:20
  • but i can still render the question text in the same way and after it i make two cases for the answer section – nikva Jan 15 '21 at 01:26
  • thanks for the help. Is it asked too much to provide the structure too? i was working on this the whole night and have to get up again in 4 hours – nikva Jan 15 '21 at 01:30
  • 1
    That sounds like an opportunity to ask another question which includes what you've already tried and what isn't working. – cbr Jan 15 '21 at 01:33
  • well, thanks anyways. you were a great help. – nikva Jan 15 '21 at 01:37