Reputation: 427
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
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
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
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..
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