I'm currently working on a clone of a memory game called Indefinite Interrogation using React (GitHub here). I'm trying to improve the algorithm for asking questions, but my recent changes to it are for some reason causing my component to freeze the webpage.
I think everything going wrong is due to some bad useState() calls but I'm not sure how to fix it.
Here's the file causing problems (ignore the massive amount of code before the function Game() component; I'm only including everything before that for anyone trying to reproduce my problem since the code below differs from the GitHub version):
import React, { useState, useEffect } from 'react'
import buttonPress from '../sounds/button_press.mp3'
import buttonWrong from '../sounds/wrong_answer.mp3'
function randomArrayElement(array) {
const randIndex = Math.floor(Math.random() * array.length);
return array[randIndex];
}
// Returns an array of length n, with no duplicates, from
// random elements in array
function getNRandomAnswers(array, n, correctAnswer = null) {
let answerSet = new Set();
while (answerSet.size < n) {
const randomAnswer = randomArrayElement(array);
if (randomAnswer !== correctAnswer) {
answerSet.add(randomAnswer);
}
}
return Array.from(answerSet);
}
// Durstenfeld shuffle; see https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
class Question {
constructor(questionText, possibleAnswers) {
this.questionText = questionText;
this.possibleAnswers = possibleAnswers;
this.hasCorrectAnswer = false;
this.correctAnswer = null;
// possibleAnswers is all the possible answer choices that can be used to
// build displayedAnswers. displayedAnswers is exactly 4 answers, with only
// 1 being the correct one
this.createDisplayedAnswers();
}
createDisplayedAnswers() {
if (this.correctAnswer !== null) {
const wrongAnswers = getNRandomAnswers(this.possibleAnswers,
3, this.correctAnswer);
this.displayedAnswers = [this.correctAnswer, ...wrongAnswers];
shuffleArray(this.displayedAnswers);
}
else {
const randomAnswers = getNRandomAnswers(this.possibleAnswers, 4);
this.displayedAnswers = [...randomAnswers];
// No shuffling needed as the answers are chosen randomly; we only
// need to shuffle to make sure the correct answer isn't always displayed
// in the same place
}
}
setCorrectAnswer(answer) {
this.correctAnswer = answer;
this.hasCorrectAnswer = true;
}
reset() {
this.correctAnswer = null;
this.hasCorrectAnswer = false;
this.createDisplayedAnswers();
}
}
const possibleNames = ['Danny James', 'Megan Jenkins',
'Alexander Patel', 'Kwabena Ihejirika', 'Mei Ueda', 'Nishant Chowdhury',
'Yejide Afolayan', 'Tina Powell', 'Hong Wu', 'Kevin Campbell', 'Ai Chen',
'Seok Yi', 'Robert Dixon', 'Jack Marshall'];
const possibleLocations = ['China', 'Canada',
'The Philippines', 'India', 'Indonesia', 'Mexico', 'Russia', 'Brazil',
'The United States', 'The United Kingdom', 'Japan', 'Italy', 'Germany'];
const possibleAges = ['51', '35', '33', '32', '37', '19', '43', '31', '38',
'24', '45', '46', '16', '49', '27', '42', '30', '52', '21'];
const possibleActivities = ['Visiting a friend', 'Drinking', 'Skydiving', 'Working',
'Playing video games', 'Getting married', 'Hiking', 'Selling drugs', 'Cooking drugs',
'Enjoying nature', 'Using the bathroom', 'Ice skating'];
const possibleNumKilled = ['0', '1', '2', '3', '4', '5-10'];
const possibleRelations = ['My grandfather', 'My brother',
'My uncle', 'My mother', 'My sister', 'My grandmother', 'My father', 'My aunt']
const possibleOccupations = ['Exobiologist', 'Computer Hardware Engineer',
'Nuclear Engineer', 'Linguist', 'Software Engineer', 'Actor', 'Chemist',
'Aerospace Engineer', 'Floral Designer', 'Architect', 'Writer', 'Civil Engineer'];
const possibleReasons = ['For political reasons', 'For a loved one',
'Just for fun', 'For a friend', 'For protection', 'Out of necessity',
'For the children'];
const possibleBuddhismReasons = [...possibleReasons, "I don't know"];
const possibleValuablesLocations = ['Under the kitchen sink', 'Behind my bookshelf',
'Under the floorboards', 'In the basement', 'The bathroom cabinet', 'Under my bed',
'My bedroom closet'];
const possibleComputerTimes = ['10:45 AM', '1:45 PM', '7:30 AM', '4:00 PM', 'Midnight',
'8:00 AM', '6:00 PM', '9:06 AM', '5:40 PM', 'Noon'];
const possibleDrugReasons = [...possibleReasons, 'It clears a guilty heart'];
const possibleReligions = ['Christianity', 'None', 'Hinduism', 'Judaism', 'Islam'];
const possibleSisterLocations = [...possibleLocations, "I don't know"];
const possibleSisterLunchTimes = ['1PM - 2PM', '10PM - 4AM', '6AM - 9PM', '5AM - 12AM',
'2PM - 3PM', '2AM - 3AM', '3AM - 4AM', '1AM - 2AM', '7AM - 3PM', '8AM - 6PM', '11AM - 12PM'];
const possibleSisterLastSeenTimes = ['2 days ago', '10 years ago', 'Before The Incident',
'Last month', '8 years ago', '9 years ago', 'Last year', 'Last week', '11 years ago', '7 years ago',
'6 years ago', 'Yesterday', 'A few hours ago'];
const possibleTalkTimes = ['Never', 'Tomorrow', 'In a few days', 'In a few weeks',
'Next week', 'In about a year', 'In several months'];
const possibleMemorySounds = ["I don't remember", 'There are four facets of mind',
'We brought you here for a reason', 'When did you exist?'];
const possibleDestinations = ['Neptune', 'The Asteroid Belt', 'Jupiter', 'Saturn',
'The Moon colony', 'Mars'];
const possibleWeapons = ['Clubs', 'Submachine guns', 'Pistols', 'Antimatter mints',
'Swords', 'Javelins'];
const possibleRestaurants = ['The White Rug', 'Trounce Steaks', 'The Dancing Crow',
'The Eating Hole', 'The Blue Daisy', "Supreme Overlord's Noodles", 'Stellar Seafood'];
const possibleStrange = ['A hooded figure', 'A glowing skyward object',
'A one-eyed man', 'A white motor vehicle'];
const possibleTerroristLeaders = ['Lady Dukkha', 'Dr. Smith', "I'm not a terrorist",
"I don't know", '...What?'];
const possibleDepartureHelpers = [...possibleRelations, 'No one!'];
const possibleDrugSellers = [...possibleRelations, 'A stranger'];
const possibleCriticismReasons = ['She was angry', 'That question is unfair',
"I don't remember", 'She felt it was important', 'For school'];
const possibleCodes = ['Sbyybj #OenaqYvory', 'Va gur qlfgbcvn2a shgher',
'Guvf zft vf abg 1frpher', 'Vaqrsvavgr frdhry'];
const list1 = [
new Question('What is your full name?', possibleNames),
new Question('How old are you?', possibleAges),
new Question('Where were you born?', possibleLocations),
new Question('Where were you during The Incident?', possibleLocations),
new Question('What were you doing the day of The Incident?', possibleActivities),
new Question('How many did you kill during The Incident?', possibleNumKilled),
new Question('Who are you closest to?', possibleRelations),
new Question('What is your occupation?', possibleOccupations),
new Question('Where in your home do you keep your valuables?', possibleValuablesLocations),
new Question('Which religion do you identify with?', possibleReligions)
]
const list2 = [
new Question('How old is your sister?', possibleAges),
new Question('Your sister converted to Buddhism. Why?', possibleBuddhismReasons),
new Question("What was your sister's occupation?", possibleOccupations),
new Question('What are the whereabouts of your sister?', possibleSisterLocations),
new Question('When does your sister usually go out for lunch?', possibleSisterLunchTimes),
new Question('When did you last see your sister in person?', possibleSisterLastSeenTimes),
new Question('When did your sister run away from home?', possibleSisterLastSeenTimes),
new Question('When were you planning to talk to us about your sister?', possibleTalkTimes),
new Question('Why have you been buying illegal drugs like SpyteFire?', possibleDrugReasons),
new Question('Who sold you illegal drugs like SpyteFire?', possibleDrugSellers),
new Question('When did you last log into a computer console?', possibleComputerTimes),
new Question('When do you usally go out for lunch?', possibleSisterLunchTimes)
]
const list3 = [
new Question("Who is your sister's significant other?", possibleNames),
new Question('Years ago, your sister criticized the government. Why?', possibleCriticismReasons),
new Question('What did you hear, just as you lost your memory?', possibleMemorySounds),
new Question('Where were you planning to go after leaving Earth?', possibleDestinations),
new Question('What kinds of weapons have you been buying?', possibleWeapons),
new Question('In which restaurant do you have your lunch meetings?', possibleRestaurants),
new Question('What period of The Incident do you have no memory of?', possibleSisterLunchTimes),
new Question('What seemed strange to you the day before The Incident?', possibleStrange),
new Question('Who is the leader of your terrorist cell?', possibleTerroristLeaders),
new Question('Before we got you, when were you planning to leave Earth?', possibleTalkTimes),
new Question('Who helped arrange your departure from Earth?', possibleDepartureHelpers),
new Question('Why were you planning to leave Earth?', possibleReasons),
new Question('You recently received a coded message. What did it say?', possibleCodes),
new Question('When did you search "overthrow government" yesterday?', possibleComputerTimes)
]
const questions = [
list1,
list2,
list3
]
const wrongAnswerReplies = ['You slipped up!', 'Liar! You contradicted yourself.',
"Your responses don't match.", 'Liar!'
]
const correctAnswerVariations = ["Here's your previous answer: ", 'This was what you said before: ',
'Earlier you said: '
]
const questionIntroVariations = ['I see.', 'Interesting.']
function getWrongAnswerReply() {
return randomArrayElement(wrongAnswerReplies) + ' ' + randomArrayElement(correctAnswerVariations);
}
function getInitialQuestionList(questionsIndices) {
let initialList = [list1[0], list1[1], list1[2]]
while (initialList.length < 10) {
const nextIndex = randomArrayElement(questionsIndices)
const nextQuestion = randomArrayElement(questions[nextIndex])
const lastInitialListIndex = initialList.length - 1
if (nextQuestion.text !== initialList[lastInitialListIndex].text) {
initialList.push(nextQuestion)
}
}
return initialList
}
function getNextQuestionList(questionsIndices) {
let nextList = []
while (nextList.length < 10) {
const nextIndex = randomArrayElement(questionsIndices)
const nextQuestion = randomArrayElement(questions[nextIndex])
const lastListIndex = nextList.length - 1
if (nextList.length === 0) {
nextList.push(nextQuestion)
}
else if (nextQuestion.text !== nextList[lastListIndex].text) {
nextList.push(nextQuestion)
}
}
}
function Game(props) {
const [questionsIndices, setQuestionsIndices] = useState([0])
const [questionList, setQuestionList] = useState(getInitialQuestionList(questionsIndices))
const [currentListIndex, setCurrentListIndex] = useState(0)
const [question, setQuestion] = useState(questionList[0])
// useEffect(() => {
// setQuestionsIndices([0])
// setQuestionList(qL => getInitialQuestionList(questionsIndices))
// setQuestion(q => questionList[0])
// setCurrentListIndex(0)
// console.log('finished')
// }, [questionList, questionsIndices])
const answers = [...question.displayedAnswers]
function getNextQuestion() {
if (props.score > 0 && props.score % 10 === 0) {
const nextQuestionsIndex = questionsIndices[questionsIndices.length - 1] + 1
if (nextQuestionsIndex < questions.length) {
setQuestionsIndices(qI => [...qI, nextQuestionsIndex])
}
setQuestionList(getNextQuestionList(questionsIndices))
setCurrentListIndex(0)
}
const currentQuestion = questionList[currentListIndex]
setCurrentListIndex(lI => lI + 1)
return currentQuestion
}
function handleClick(answerIndex) {
// Prevents users from continuing to further questions after wrong answers
if (props.isGameOver) { return; }
// console.log('Test: ' + answerIndex);
const chosenAnswer = answers[answerIndex]
if (!question.hasCorrectAnswer) {
const buttonSound = new Audio(buttonPress)
buttonSound.play()
question.setCorrectAnswer(chosenAnswer)
// Make sure the answers aren't displayed the same way next time
question.createDisplayedAnswers()
setQuestion(getNextQuestion())
props.setScore(props.score + 1)
}
else if (chosenAnswer === question.correctAnswer) {
const buttonSound = new Audio(buttonPress)
buttonSound.play()
question.createDisplayedAnswers()
setQuestion(getNextQuestion())
props.setScore(props.score + 1)
}
else {
const wrongAnswerSound = new Audio(buttonWrong)
wrongAnswerSound.play()
props.setGameOver(true);
setTimeout(() => {
// This forEach is required to reset the Question answers;
// otherwise they'll keep the correctAnswer from the previous session!
questions.forEach(q => {
q.reset();
})
setQuestion(questions[0]);
props.setGameOver(false);
props.setScore(0);
// Return to Main Menu
props.setGameStarted(false);
}, 3000)
}
}
function getQuestionText() {
let displayedText = ''
const randomNum = Math.random() * 100
// 0-9 is ten numbers, so this should have a 10% chance of being called
// props.score can't be 0; otherwise this might get called for the very first question!
// That wouldn't match the original game's behavior...
if (randomNum < 10 && props.score !== 0) {
displayedText = randomArrayElement(questionIntroVariations) + ` ${question.questionText}`
}
else {
displayedText = question.questionText
}
return displayedText
}
return (
<>
<p>{props.score}</p>
<p className="game-question">{props.isGameOver ? `${getWrongAnswerReply()} ${question.correctAnswer}`
: getQuestionText()}</p>
<div className="container">
<div className="row row-eq-height">
<div className="col">
<button className="btn button"
onClick={() => handleClick(0)}>{answers[0]}</button>
</div>
<div className="col">
<button className="btn button"
onClick={() => handleClick(1)}>{answers[1]}</button>
</div>
</div>
<div className="row row-eq-height">
<div className="col">
<button className="btn button"
onClick={() => handleClick(2)}>{answers[2]}</button>
</div>
<div className="col">
<button className="btn button"
onClick={() => handleClick(3)}>{answers[3]}</button>
</div>
</div>
</div>
</>
);
}
export default Game;
I tried useEffect to solve my problem but that didn't work. Sorry for any bad coding/styling practices; I'm new to functional React components and I have a bad habit of using semi-colons even though I don't actually have to.
(Adding this as a formal answer since it solved the OP's issue).
The bug is in the getInitialQuestionList() function, which currently contains an infinite loop - the length of the array never increases. As you yourself identified, you were using the wrong .text property on the question instead of the correct .questionText.
How I found out the problem
I copied and pasted your code into a new React CodeSandbox, which showed me a nice error message saying there was a likely infinite loop and the exact line in the function. I just didn't dig into it long enough after that to figure out why the infinite loop was happening, though.
P.S. If you were using TypeScript here, the TS Compiler likely would have caught this error and saved you a lot of bother. Just food for thought.
I'm new to React js & React Native please help
so i have this data of partners
const [lessorPartners , setLessorPartners] = useState(null)
const dataPartnerBeforeFetch = [
{id:1, name:"BFI" , img:"lessor1" , code:"PU77"},
{id:2, name:"SMF" , img:"lessor2" , code:"TT38"},
{id:3, name:"Adira" , img:"lessor3" , code:"PT74"},
{id:4, name:"BFI" , img:"lessor1" , code:"PB63"},
{id:5, name:"SMF" , img:"lessor2" , code:"BU42"},
{id:6, name:"Adira" , img:"lessor3" , code:"AL39"}
]
useEffect(() =>{
if(dataPartnerBeforeFetch ){
dataPartnerBeforeFetch.map(dataPartner=>{
dataPartner.color = false
})
setLessorPartners(dataPartnerBeforeFetch)
}
},[dataPartnerBeforeFetch ])
I added color to its end if its true then it will turn transparent / false it will be orange
and I tried to loop it :
and render it all with these functions
const renderingPartners = () => {
return(
lessorPartners.map(lessorPartner => {
renderingPartner(lessorPartner)
})
)
}
const renderingPartner = (lessorPartner) =>{
return(
<div style={{backgroundColor: false ? 'orange' : 'transparent'}}
onClick={()=>{
onClickParter(lessorPartner);
}}
>
<LessorPartner
key = {lessorPartner.id}
object = {lessorPartner}
/>
</div>
)
}
and i tried to call renderingPartners() in my app .js like this
<div>
{ lessorPartners && renderingPartners()}
</div>
but no component returned, just empty and no error
the next idea is to change it's color on click with this function and re render the whole mapping
const onClickParter = (q) =>{
q.color = !q.color
let index = lessorPartners.indexOf(q);
lessorPartners[index]= q
setLessorPartners(lessorPartners)
renderingPartners()
}
just like radio button with list of lessor that i've tried to map
please help i've been stuck here for hours
So map function returns a new array (also it returns a value so if you are specifying braces you have to explicitly write return). you should modify your first snippet like this:
let _dataPartnerBeforeFetch = dataPartnerBeforeFetch.map(dataPartner=>{
return {
...dataPartner,
color : false
}
});
setLessorPartners(_dataPartnerBeforeFetch)
Similarly this snippet should be corrected like this:
lessorPartners.map(lessorPartner => renderingPartner(lessorPartner))
Edited:
Why the color change is not reflected?
Its almost always best to return a new array.
const onClickParter = (q) =>{
let _lessorPartners = lessorPartners.filter(f=> !(f.indexOf(q) >= 0));
_lessorPartners.push({
...q,
color : !q.color
});
setLessorPartners(_lessorPartners);
//renderingPartners();
//we donot have to explicity call the function to enforce rerender because it's already binded by a state variable. So setting the state would do the trick.
}
Here filter returns a new copy of the array without the item in scope (reffered to as 'q'). Then you add a new object with the inverted color to the new array and set the state.
I'm starting at Angular and I'm having a really hard time setting a FormArray inside my form. In summary what I did was:
Create 2 forms: the main (valForm) and another one created dynamically (holidaysForm).
Copy the values of the second form to an Array.
Load the values of the Array into a FormArray.
Follow the codes:
.ts
let group = {}
this.holidaysForm = new FormGroup(group);
this.valForm = fb.group({
dentistId: [null, Validators.required],
initialHour: [null, Validators.required],
endHour: [null, Validators.required],
numberOfHolidays: [null],
appointmentsInterval: [null, Validators.required],
year: [null, Validators.required],
workDays: this.buildDays(),
holidays: this.buildHolidays()
});
buildDays() {
const values = this.workDays.map(v => new FormControl(false));
return this.fb.array(values);
}
buildHolidays() {
if (typeof this.valForm !== 'undefined') {
let teste = Object.assign({}, this.holidaysForm.value);
this.holidays = new Array();
Object.values(teste).forEach((v) => {
this.holidays.push(v);
})
console.log(this.holidays);
const values = this.holidays.map((v)=> new FormControl(v));
return this.fb.array(values);
}
}
dynamicForm(val) {
if (val > 365) {
val = 365;
this.valForm.patchValue({
numberOfHolidays: 365
})
}
const val_plus_one = parseInt(val) + 1
let i: number = val_plus_one;
if (i < this.oldNumber) {
do {
this.holidaysForm.get('holiday' + i.toString()).setValue(null);
this.holidaysForm.removeControl('holiday' + i.toString());
i++
} while (i <= this.oldNumber)
}
const numbers = Array(val).fill(val, 0, 365).map((_, idx) => idx + 1)
numbers.forEach((num) => {
const fc = new FormControl('', FormValidations.Calendar(this.valForm.get('year').value));
this.holidaysForm.addControl('holiday' + num.toString(), fc)
})
this.numberOfHolidays = numbers;
this.oldNumber = val;
}
.html
<div class="col-md-4 mda-form-group">
<input class="mda-form-control" type="number" min="0" max="365"
formControlName="numberOfHolidays"
(change)="dynamicForm(valForm.get('numberOfHolidays').value)" />
<label>Total de Feriados</label>
</div>
<div [formGroup]="holidaysForm">
<div class="mda-form-group" *ngFor="let num of numberOfHolidays">
<input class="mda-form-control" type="date" formControlName="holiday{{num}}" />
<label>Feriado {{num}}</label>
<app-error-msg [control]="holidaysForm.get('holiday'+num)" label="Feriado">
</app-error-msg>
</div>
</div>
In theory the code works well and without errors, the problem is that the result is always this:
Main Form
Valores:
{
"dentistId": null,
"initialHour": null,
"endHour": null,
"numberOfHolidays": 3,
"appointmentsInterval": null,
"year": null,
"workDays": [
false,
false,
false,
false,
false,
false,
false
],
"holidays": null
}
Holiday Form
{
"holiday1": "2020-01-01",
"holiday2": "2020-03-01",
"holiday3": "2020-02-01"
}
Does anyone have any idea what I might be doing wrong? Thankful,
The problem seems to be you are using holidaysForm before you ever put values in it. So at the point where you are creating the object to loop through to create your array, that object will have no properties.
buildHolidays() {
if (typeof this.valForm !== 'undefined') {
let teste = Object.assign({}, this.holidaysForm.value); // this line
this.holidays = new Array();
Object.values(teste).forEach((v) => {
this.holidays.push(v);
})
console.log(this.holidays);
const values = this.holidays.map((v)=> new FormControl(v));
return this.fb.array(values);
}
}
I would figure out how to get values to use in this method to make it all work. And remember, you can always add a formArray after the creation of your original formgroup via group.addControl('controlName', myFormArray);
Best of luck, and as always
Happy Coding