Robert O'Toole
Robert O'Toole

Reputation: 427

react rendering and state

While I understand the use of state and rendering a component, I'm having trouble with this particular situation.

I have a set of questions and I want to pick a random number from 0 to the question length (in my case 8) without the numbers repeating.

I have figured that logic out but when I assign the random numbers to the state it seems that the re-rendering is causing this logic to reset each time and thus the numbers repeat. i need to THEN link to the question id with the corresponding random number. or something of this nature.

const SomeComponent = props => {
  const [random, setRandom] = useState(null)

  function getRandomNumber(min, max) {
    let stepOne = max - min + 1;
    let stepTwo = Math.random() * stepOne;
    let result = Math.floor(stepTwo) + min;

    return result
  }

  // this creates the array[0, 1, 2, 3, 4, 5, 6, 7]
  function createArrayOfNumbers(start, end) {
    let myArray = [];

    for (let i = start; i <= end; i++) {
      myArray.push(i)
    }

    return myArray
  }

  let numbers = []

  function generator() {
    let numbersArray = createArrayOfNumbers(0, qAndA.length - 1)
    let finalNumbers = [];

    while (numbersArray.length > 0) {
      let randomIndex = getRandomNumber(0, qAndA.length - 1)
      let randomNumber = numbersArray[randomIndex];
      numbersArray.splice(randomIndex, 1);
      finalNumbers.push(randomNumber)
    }
    for (let nums of finalNumbers) {
      if (typeof nums === 'number') {
        numbers.push(nums)
        // for some reason i was getting undefined for a few so i weeded them out here and pushed into a new array
      }
    }
    const tester = numbers.splice(0, 1) ** this part works fine and
    console.log(tester) // each time my button is pressed it console logs a non repeating random number until ALL numbers(from 0 to 7 are chosen)
    setRandom(tester) // THIS IS THE LINE THAT SCREWS IT UP.re rendering seems to toss all the previous logic out...
  }


  return (<button onClick={generator}>click this to call function</button>)
}

Everything up to the last line works.

It's giving me a random number and the button I have which executes the function (thus giving the random number)

DOES NOT repeat the random numbers and gives me something like 0 then 4 then 1 etc. each time I click it until it gives me all the possible numbers from 0 to 7.

But when I include the last line which sets the state to those random numbers each click it seems the re-rendering of the page resets this entire function, thus forgetting to NOT repeat and forgetting all the previous logic.

To clarify something: this needs to be done with state because I want to set a random question to this random number state, then render a random question without it repeating (think of a basic quiz).

I also do not want the amount of numbers set or determined. It needs to work dynamically considering I will be adding more questions over time to the quiz.

Upvotes: 1

Views: 170

Answers (3)

Ievgen
Ievgen

Reputation: 4443

Try this hook to store visited numbers list:

const [numbers, setVisited] = useState([]) .....

add your number to a "visited" array and clone an array on a button click (react hacks/ways to rerender, while references are compared)

setVisited(numbers.slice(0))

Upvotes: 0

Lynden Noye
Lynden Noye

Reputation: 1011

Check out this demo I threw together: https://codesandbox.io/s/competent-pine-0hxxi?file=/src/index.tsx

I understand I'm not answering your question about your current code directly, but I think your high-level issue is that you're approaching the problem from the wrong direction.

Your approach looks like this:

component => create data => render

When often the best way to do it looks like this:

receive data => component => render

I think your problem is actually "How do I shuffle an array of items?". The rest of your problem then lies with how you decide to present it, and response to user interaction.

Before we start thinking about your component, your questions are defined somewhere. Let's call this initial pool of questions data.

Your component will then accept this data either as props, or it might fetch them from the network. Where ever they come from, it doesn't really matter.

With this data, we can say "OK our initial state for presentation is a random ordering of this data, aka 'shuffled'".

  // given our question pool `data`
  // we can simply set the initial state to a shuffled version
  const [questions, setQuestions] = React.useState<Question[]>(
    shuffle([...data])
  );

OK, so we have our 'random' (shuffled) questions. We don't need to worry about this at all anymore.

If I've missed the fact you want it to continue to shuffle the questions as each gets answered, happy to expand on my answer further.

Now all we need to do is display them how ever we like. If we're only showing one question at a time, we need to keep track of that.

  // keep track of which question we're displaying right now
  const [qIndex, setQIndex] = React.useState<number>(0);

When a user selects or gives an answer to the question, we can simply replace that question with our answered question. This is how React state likes to work; don't mutate what you already have, just throw everything at it all over again.

  const handleAnswerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    // create our updated question
    // now with an answer
    const theQuestion = questions[qIndex];
    const answeredQuestion = {
      ...theQuestion,
      answer: event.target.value
    };
    // copy our questions, and flip the old question for the new one
    const newQuestions = [...questions];
    newQuestions.splice(qIndex, 1, answeredQuestion);
    setQuestions(newQuestions);
  };

The rest just rests on letting users navigate through your array of questions. Here's the full component:

interface QuizProps {
  data: Question[];
}

export const Quiz = (props: QuizProps) => {
  // keep track of which question we're displaying right now
  const [qIndex, setQIndex] = React.useState<number>(0);

  // given our question pool `data`
  // we can simply set the initial state to a shuffled version
  const [questions, setQuestions] = React.useState<Question[]>(
    shuffle([...props.data])
  );

  // create our updated question
  // now with an answer
  const handleAnswerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const theQuestion = questions[qIndex];
    const answeredQuestion = {
      ...theQuestion,
      answer: event.target.value
    };
    // copy our questions, and flip the old question for the new one
    // using slice (there are many ways to do this)
    const newQuestions = [...questions];
    newQuestions.splice(qIndex, 1, answeredQuestion);
    setQuestions(newQuestions);
  };

  const handleBackClick = () => setQIndex((i) => (i > 0 ? i - 1 : 0));
  const handleNextClick = () =>
    setQIndex((i) => (i < questions.length - 1 ? i + 1 : i));

  return (
    <div>
      <h1>Quiz</h1>
      <div>
        <h2>{questions[qIndex].title}</h2>
        <h3>
          Question {qIndex + 1} of {questions.length}
        </h3>
        <p>{questions[qIndex].description}</p>
        <ul>
          {questions[qIndex].options.map((answer, i) => (
            <li key={i}>
              <input
                id={answer.id}
                type="radio"
                name={questions[qIndex].id}
                checked={answer.id === questions[qIndex]?.answer}
                value={answer.id}
                onChange={handleAnswerChange}
              />
              <label htmlFor={answer.id}>{answer.value}</label>
            </li>
          ))}
        </ul>
      </div>
      <button onClick={handleBackClick}>Previous</button>
      <button onClick={handleNextClick} disabled={!questions[qIndex].answer}>
        Next
      </button>
    </div>
  );
};

Upvotes: 1

Drew Reese
Drew Reese

Reputation: 202618

The issue is that your previous random numbers are wiped each render cycle. You can "persist" them outside the react lifecycle.

Create a function that generates a random value and maintains its own cache of last random number generated.

const randomNumberGenerator = () => {
  let previous;

  return max => {
    let randomNumber;
    do {
      randomNumber = Math.floor(Math.random() * max);
    } while(randomNumber === previous);
    previous = randomNumber;
    return randomNumber;
  }
};

Then in your react component when you need to grab a random, non-consecutive value:

const getRandomNumber = randomNumberGenerator();

...

getRandomNumber(8)
getRandomNumber(10)
// etc..

Edit react-rendering-and-state

If you know your max will never change, i.e. it will always be 8, then you can move max to the outer factory function.

const randomNumberGenerator = max => {
  let previous;

  return () => {
    let randomNumber;
    do {
      randomNumber = Math.floor(Math.random() * max);
    } while(randomNumber === previous);
    previous = randomNumber;
    return randomNumber;
  }
};

Then the usage in code is simplified a little.

const getRandomNumber = randomNumberGenerator(8);

...

getRandomNumber()

If you still need to handle a range of random numbers then add a min value to the function and compute the random value in range.

const randomNumberGenerator = () => {
  let previous;

  return (min, max) => {
    let randomNumber;
    do {
      randomNumber = Math.floor(Math.random() * (max - min) + min);
    } while (randomNumber === previous);
    previous = randomNumber;
    return randomNumber;
  };
};

Upvotes: 0

Related Questions