简体   繁体   中英

Randomizing quiz-answers fetched from a rest-API

So I've created a quiz-app that fetches 10 random questions with three incorrect and one correct answer to choose from. Everything is working great at the moment except that I can't get the answers to pop up randomly. And by that I mean that the correct answer is always at the bottom of the choices presented.

I know that the answer is Math.floor(Math.random() * ... ) but I honestly have no idea where to put it. I've tried everything. I could really use some help.

import React, { Component } from "react";
import "./App.css";

const API =
  "https://opentdb.com/api.php?amount=10&category=20&difficulty=medium";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      results: [],
      score: []
    };
  }

  componentDidMount() {
    this.populateAppWithData();
  }

  populateAppWithData() {
    fetch(API)
      .then(response => response.json())
      .then(data => this.setState({ results: data.results }))
      .catch(error => console.error(error))
  }

  render() {
    const { results } = this.state;
    return (
      <div className="App">
        <h1>Quiz App</h1>
        <TheCounter results={results}
          Counter={this.state.score}
        />
      </div>
    );
  }
}
class MythologyAnswers extends Component {
  constructor(props) {
    super(props);
    this.state = {
      answered: "",
      isRight: null,
    };
  }
  answerClicked(answer) {
    const { hasAnswered, correct_answer } = this.props;
    return event => {
      const isRight = correct_answer === answer;
      hasAnswered(isRight);
      this.setState({
        answered: answer,
        isRight,
      });
    };
  }

  render() {
    const { question, correct_answer, incorrect_answers } = this.props;
    const { answered, isRight } = this.state;
    return (
      <div className="allAnswers">
        {question}
        {incorrect_answers && incorrect_answers
          .concat(correct_answer)
          .map(answer => (<div onClick={this.answerClicked(answer)}>{answer} </div>))}<br />
        {answered && `You answered ${answered}`} {" "} <br />
        <div className="correctAnswer"> {" "}{answered && isRight && "This is correct!"} </div> <br />
        <div className="incorrectAnswer"> {" "}{answered && !isRight && `This is incorrect. The correct answer is ${this.props.correct_answer}`} {" "}</div>
      </div>
    )
  }
}
class TheCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      right: 0,
      Counter: 0,
      unanswered: 0,
    };
  }
  questionAnswered = isRight => {
    this.setState(({ Counter, right }) => ({
      Counter: Counter + 1,
      right: right + isRight,
    }));
  }
  render() {
    const { results } = this.props;
    const { Counter } = this.state;
    const unanswered = this.props.results && Counter;
    if (unanswered >= 10) {
      return <div className="theUnanswered"> You got {this.state.right} right out of {this.state.Counter} </div>;
    }
    const question = results[Counter];
    return (
      <div className="newQuestion">
        <MythologyAnswers {...question} hasAnswered={this.questionAnswered} />
      </div>
    )
  }
}
export default App;

In below code , I suppose you are trying to concat incorrect_answer array with the correct_answer

incorrect_answers && incorrect_answers
          .concat(correct_answer)

so array becomes [incorrect_answers,incorrect_answers,incorrect_answers,correct_answer] ie correct answer is at end so if you want to insert correct answer at random position and suppose there are 4 options then first generate a random number between 0 and 3

let randonIndex = Math.floor(Math.random() * 4)

then insert correct answer at random index

incorrect_answers && incorrect_answers
          .splice(randomIndex, 0, correct_answer);

We just need to apply the randomizer in the right place. You use .concat() to combine the two arrays, so it makes sense to use the randomizer right after that and before we call .map()

I've set up something that preserves alot of the existing logic you already wrote.

This will help create the new Arr and set-up the markup for your component.

  renderChoices = () => {
    const { correct_answer, incorrect_answers } = this.props;

    let allAnswers = incorrect_answers
      ? incorrect_answers.concat(correct_answer)
      : [];

    //simple shuffle algorithm. Just inject your array and it'll pop out a new one.

    function createRandom(arr) {
       let myArr = [...arr];  //copy arr we pass in
       let randomizedArr = []; //gets popuated by loop

       while (myArr.length > 0) {
          var randomIndex = Math.floor(Math.random() * myArr.length); //create random number
          randomizedArr.push(myArr[randomIndex]); //add choice randomly to arr
          myArr.splice(randomIndex, 1); //cut out a piece of the array then resart loop
      }
       //when loop has finished, return random array
       return randomizedArr;
    }

    //call randomizer and get new Arr
    let randomizedArr = createRandom(allAnswers); 

    //use .map to create markup with randomizedArr
    return randomizedArr.map(answer => {
      return <div onClick={this.answerClicked(answer)}>{answer}</div>;
    });
  };

So if you were to call the above function inside render , it will create the answer-set for you.

  render() {
    const { question } = this.props;
    const { answered, isRight } = this.state;
    return (
      <div className="allAnswers">

        {question}

        { this.renderChoices()}

        <br />

        {answered && `You answered ${answered}`} <br />

        <div className="correctAnswer">
          {answered && isRight && "This is correct!"}
        </div>

        <br />

        <div className="incorrectAnswer">
          {answered &&
            !isRight &&
            `This is incorrect. The correct answer is ${
              this.props.correct_answer
            }`}
        </div>
      </div>
    );
  }

Clean and not too-complex :)

Edit: so without changing your original code too much:

  createRandom(arr) {
    let myArr = [...arr]; //copy arr we pass in
    let randomizedArr = []; //gets popuated by loop

    while (myArr.length > 0) {
      var randomIndex = Math.floor(Math.random() * myArr.length); //create random number
      randomizedArr.push(myArr[randomIndex]); //add choice randomly to arr
      myArr.splice(randomIndex, 1); //cut out a piece of the array then resart loop
    }
    //when loop has finished, return random array
    return randomizedArr;
  }

  render() {
    const { question, correct_answer, incorrect_answers } = this.props;
    const { answered, isRight } = this.state;
    const allAnswers =
      incorrect_answers ? incorrect_answers.concat(correct_answer) : [];
    const randomizedAnswers = this.createRandom(allAnswers)

    return (
      <div className="allAnswers">
        {question}
        {randomizedAnswers
            .map(answer => (
              <div onClick={this.answerClicked(answer)}>{answer} </div>
            ))}
        <br />
        {answered && `You answered ${answered}`} <br />
        <div className="correctAnswer">
          {answered && isRight && "This is correct!"}
        </div>
        <br />
        <div className="incorrectAnswer">

          {answered &&
            !isRight &&
            `This is incorrect. The correct answer is ${
              this.props.correct_answer
            }`}
        </div>
      </div>
    );
  }

So in the edited version we do a couple things:

  1. Defined a function called createRandom() ... all it does is randomize your answers choices.
  2. In render , we created a new variable called allAnswers which just concat() the incorrect_answers and correct_answer like you did previously. If there are no incorrect_answers defined, we will use an empty array [] as the default value.
  3. Create a new variable called randomizedAnswers . We call createRandom() and pass in allAnswers as the argument. It returns the randomized array for us to use.
  4. Then just .map() over randomizedAnswers to create your answer-choices markup.

I'd personally add it right after fetching the data. Using an inline shuffle like Fisher-Yales you don't need to modifiy your structure at all.

const fisherYalesShuffle = (a) => {
    let j, x, i;
    for (i = a.length - 1; i > 0; i--) {
        j = Math.floor(Math.random() * (i + 1));
        x = a[i];
        a[i] = a[j];
        a[j] = x;
    }
}

However, since you generally want to move as much data-logic as possible to your backend, you should also consider shuffling them there instead.

You can define a shuffle function (This one is Fisher-Yates (aka Knuth) shuffle):

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

And then just apply the function when setting the state like this:

this.setState({ results: this.shuffle(data.results) })

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM