The quiz does not block the questions answered - javascript

The quiz generally works ok, but when I click 2 times in the same answer, I count the points 2 times, like I would click 100 times
in the same correct answer I have 100 points. I don't know how to fix it .. Please help ...
QuestionBox:
const QuestionBox = ({ question, options, selected }) => {
const [answer, setAnswer] = useState(options);
return (
<div className="questionBox">
<div className="question">{question}</div>
{(answer || []).map((text, index) => (
<button key={index} className="answerBtn" onClick={() => {
setAnswer([text]);
selected(text)
}}>{text}</button>
))}
</div>
)
}
computeAnswer:
computeAnswer = (answer, correctAnswer) => {
if (answer === correctAnswer) {
this.setState({
score: this.state.score + 1,
})
render:
{this.state.qBank.map(
({ question, answers, correct, id }) => (
<QuestionBox key={id} question={question} options={answers} selected={Answers => this.computeAnswer(Answers, correct)} />
)
)}

You can have a boolean flag in QuestionBox state, initialized to false, and switched to true on the first click, then bypass score calculation if this flag is true :
const QuestionBox = ({ question, options, selected }) => {
const [answer, setAnswer] = useState(options);
const [alreadyAnswered, setAlreadyAnswered] = useState(false);
return (
<div className="questionBox">
<div className="question">{question}</div>
{(answer || []).map((text, index) => (
<button disabled={alreadyAnswered} key={index} className="answerBtn" onClick={() => {
if(!alreadyAnswered) {
setAnswer([text]);
selected(text);
setAlreadyAnswered(true);
}
}}>{text}</button>
))}
</div>
)
}
I also add disabled attribute if question has already been answered to let
user knows that this is one-time only.
Also it would be a good idea to put the onClick logic in a function to improve performance (https://medium.com/#Charles_Stover/cache-your-react-event-listeners-to-improve-performance-14f635a62e15).
By the way you should avoid to init state with props : React component initialize state from props

Currently, answer will either be an array of possible answer strings, or an array containing the single answer chosen by the user. You could change it so that the click listener is only attached if the length of the answer is greater than 1:
<button key={index} className="answerBtn" onClick={answer.length === 1 ? null : () => {
// rest of the code
This way, once the user chooses an answer, further clicks on the (now single rendered) button won't do anything, and the score will only be (possibly) incremented the first time the button is clicked.
answer is a bit of a strange variable name for a collection of answer strings - perhaps rename it to answerOptions or possibleAnswers or something like that, for better readability.

Related

Generate a new item in an array in React?

I'm building a to-do list and I want each list item to have a number, starting from 1. I'm using the useState hook on the counter but I can't figure out how to add new items to the array every time I click on a button. I haven't coded in months and I'm really rusty.
function Product() {
const [input, setInput] = useState("");
const [todo, setTodo] = useState([]);
const [count, setCount] = useState([]);
const addTodo = e => {
e.preventDefault();
setTodo([...todo, input]);
setCount(prevCount => [...prevCount, prevCount + 1]);
setInput("");
};
return (
<div>
<form>
<input value={input} onChange={e => setInput(e.target.value)} type='text' />
<button type='submit' onClick={addTodo}>
Add!
</button>
</form>
<h2 style={{ marginBottom: "0.5rem" }}>List of To-dos !</h2>
{todo.map(todo => (
<p>
{count.map(count => (
<p>{count}</p>
))}
{todo}
</p>
))}
</div>
);
}
I want to make it so each time I add a list item, it adds its number to the left. The first item would have 1, the second 2, etc..
This is really bad practice:
What you want to do is use the index param that Javascript map function gives you:
Good practice
todo.map((todo, index) => (
<p>{index} - {todo}</p>
))
Output
0 - walk dog
1 - do yoga
Now if want the index to start at 1, you can simply add +1 to index
todo.map((todo, index + 1) => (
<p>{index} - {todo}</p>
))
Output
1 - walk dog
2 - do yoga
Since the index values are unique, you could use that to your benefit when performing other actions such as deleting etc. Usually you add the key attribute to the individual child values according to the official React documentation as follows
todo.map((todo, index + 1) => (
<p key={index + 1}>{index} - {todo}</p>
))
where key is a unique value.
Also, change your variable names to something more meaningful.
I'd suggest changing todo to plurial todos.
Your final code should look like this:
function Product() {
const [input, setInput] = useState("");
const [todo, setTodo] = useState([]);
const addTodo = e => {
e.preventDefault();
setTodo([...todo, input]);
setInput("");
};
return (
<div>
<form>
<input value={input} onChange={e => setInput(e.target.value)} type='text' />
<button type='submit' onClick={addTodo}>
Add!
</button>
</form>
<h2 style={{ marginBottom: "0.5rem" }}>List of To-dos !</h2>
{todo.map((count, index + 1) => (
<p key={index + 1}>{index} {todo}</p>
))}
</div>
);
}
Don't.
There's no reason to track the count of items in an array as its own separate state value.
Arrays have a length property which tells you the count of items in the array.
.map() includes an index in the callback, so if you just want to output the array index then you can do that.
You certainly don't need an array of numbers from 1-X in order to know the numbers from 1 to X. The value of X alone gives you this information.
Remove the count state value entirely, and just output the index of the array from the todo list:
{todo.map((t, x) => (
<p>
{x}
{t}
</p>
))}
Note also that I abbreviated your inner todo variable to just t. You can call it whatever you like, but giving it the same name as another variable you already have is just asking for confusion and bugs.
You may instead want to rename your array to something plural, like todos. Then each item therein is semantically a todo:
{todos.map((todo, x) => (
<p>
{x}
{todo}
</p>
))}
Basically, names are important. A variable/type/property/etc. name should tell you exactly and unambiguously what it is. Poor variable names lead to confusing code, and being confused about your code is exactly what brought you here.

React useState pre populated from previous component render

I'm trying to create a Quiz component rendering one Question at time and changing it when the user chooses one of the alternatives.
However, every time it renders the next Question it has already the chosenOption variable set from the previous Question. This happens because before I change the Question, I set the new state of the current Question with that chosenOption and strangely(to me) this is already set when the next Question component is rendered.
For me, the setChosenOption would set only for the current Question and when the Quiz renders the next Question its chosenOption would be null initially. I may be missing something from how functional components render... So why is it happening?
Thanks in advance!
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = React.useState(0)
const [answers, updateAnswers] = React.useState({})
const numberOfQuestions = questions.length
const onChoosenOptionCallbackHandler = ({ hasChosenCorrectOption, chosenOption}) => {
updateAnswers(prevAnswers => ({...prevAnswers, [currentQuestion]: hasChosenCorrectOption }))
setCurrentQuestion(currentQuestion + 1)
}
return (
<QuizBackground>
<QuizContainer>
<Question
question={questions[currentQuestion]}
index={currentQuestion}
numberOfQuestions={numberOfQuestions}
onChoosenOptionCallback={onChoosenOptionCallbackHandler}
/>
</QuizContainer>
</QuizBackground>
)
}
Here, apart from the first Question, the 'Chosen Option: ' log always show the chosenOption from the previous Question rendered and not null.
const Question = ({ question, index, numberOfQuestions, onChoosenOptionCallback }) => {
const [chosenOption, setChosenOption] = React.useState(null)
console.log('Chosen Option: ', chosenOption)
const hasChosenCorrectOption = chosenOption !== null ? (chosenOption == answer) : false
const selectOption = (optionIndex) => {
setChosenOption(optionIndex)
console.log('SELECTED: ', optionIndex, hasChosenCorrectOption, chosenOption)
onChoosenOptionCallback({ hasChosenCorrectOption, optionIndex })
}
return (
{/* I removed other parts not relevant. The RadioOption goes inside a map() from the question alternatives */}
<RadioOption
questionName={questionName}
option={option}
chosen={chosenOption === index}
onSelect={() => selectOption(index)}
key={index}
/>
)
}
Your issue is a result of not assigning keys to your Question components, that are being rendered using a map function.
The omission of proper keys (i.e. a unique property of each element in the rendered array) results in all sorts of weird behaviours, such as what you were describing.
The reason for that is that React uses these indices to optimize, by re-rendering only the components whose props were changed. Without the keys the whole process isn't working properly.

useState() in react does not update the data - is this the sort() issue?

when I click on the button to sort the data about countries after the website is loaded then the data displays correclty. But when I want to sort it again by different value, the data doesn't update and the state also doesn't update, however, the function works well, as I can see that the results in the console are sorted correctly. What can be the reason for this weird behaviour? Thank you for your time.
function AllCountries({ countries }) {
const [sortedCountries, setSortedCountries] = useState([]);
useEffect(() => {
console.log(sortedCountries)
}, [sortedCountries])
const sortResults = (val) => {
let sortedResults = countries.sort((a, b) => {
if (val === "deaths") {
return b.TotalDeaths - a.TotalDeaths;
} else if (val === "cases") {
return b.TotalConfirmed - a.TotalConfirmed;
} else if (val === "recovered") {
return b.TotalRecovered - a.TotalRecovered;
}
});
console.log(sortedResults);
setSortedCountries(sortedResults);
};
return (
<div className="all-container">
<p>Sort countries by the highest number of:</p>
<button onClick={() => sortResults("deaths")}>Deaths</button>
<button onClick={() => sortResults("cases")}>Cases</button>
<button onClick={() => sortResults("recovered")}>Recoveries</button>
<ul className="all-countries">
{sortedCountries.map((country, key) => {
return <li key={key}>{country.Country}</li>;
})}
</ul>
</div>
);
}
export default AllCountries;
Array.sort() method doesn't return new reference, it returns the same reference. So I assume in sortedCountries state is stored the same props.countries reference, that is why second button click doesn't set the state (it is the same reference), I can give you simple solution just change this line setSortedCountries(sortedResults) with this setSortedCountries([...sortedResults]) in this case copy of the sortedResults will be passed to setSortedCountries (new reference).
let sortedResults = countries.sort
this line is sorting the countries props and setting sortedResults to that pointer
Let us assume that pointer is held at mem |0001| for simplicity
After your first click, the function is fired, the prop is sorted, and sortedResults is set via setSortedCountries (set state).
This fires off the render that you desire, because the previous state was undefined, and now the state is pointing to |0001|
When your function runs again, the sort function fires off, does its work on |0001| and returns -- you guessed it -- |0001| but with your new sorted array.
When you go to set the state a second time, there wasn't actually any state changed because the previous country is |0001| and the country you want to change to is |0001|
So what can we do my good sir?
First I need you to think about the problem for a second, think about how you could solve this and try to apply some changes.
What you can try to do is copy the countries props to a new array pointer with the same values, and then sort it, and then set sortedCountries state to that list.
Does that make sense?
By the way, this is for similar reasons why if you try to setState directly on an new object state, the new object state will be exactly equal to the one you set. There isn't any real magic going on here. React does not automagically merge your previous object state and your new one, unless you tell it explicitly to do so.
In some sense you have told react to check differences between two states, an old country and a new country state (behind the scenes with the Vdom), but to react, those two states have no difference. And since the two object states have no difference, there will be no actual DOM changes.
You setting a pointer twice will therefore produce no actual changes.
You must therefore set state to an actual new pointer with new values, so the react virtual dom can compare the two states (the previous countries list) and the new countries list.
Hope that helps
-
It's a bit problem in React and another solution is by using the useReducer instead. such as below:
function reducer(state, action) {
return [...state, ...action];
}
function AllCountries({ countries }) {
const [sortedCountries, setSortedCountries] = useReducer(reducer, []);
useEffect(() => {
console.log(sortedCountries)
}, [sortedCountries])
const sortResults = (e, val) => {
e.preventDefault();
let sortedResults = countries.sort((a, b) => {
if (val === "deaths") {
return b.TotalDeaths - a.TotalDeaths;
} else if (val === "cases") {
return b.TotalConfirmed - a.TotalConfirmed;
} else if (val === "recovered") {
return b.TotalRecovered - a.TotalRecovered;
}
});
console.log(sortedResults);
setSortedCountries(sortedResults);
};
return (
<div className="all-container">
<p>Sort countries by the highest number of:</p>
<button onClick={(e) => sortResults(e, "deaths")}>Deaths</button>
<button onClick={(e) => sortResults(e, "cases")}>Cases</button>
<button onClick={(e) => sortResults(e, "recovered")}>Recoveries</button>
<ul className="all-countries">
{sortedCountries.map((country, key) => {
return <li key={key}>{country.Country}</li>;
})}
</ul>
</div>
);
}
export default AllCountries;

Why is this variable not considered to be 0 despite debugger saying otherwise?

I'm writing a React component that takes a total number of reviews as a prop. When the number of reviews is 0, I want to render an element stating
<div>No reviews yet.</div>
Otherwise, I render elements containing review data. Here is the component and its context:
const Stats = ({good, neutral, bad, totalReviews}) => {
if ({totalReviews} === 0)
return <div>No reviews yet.</div>
else {
return (
<div>
<Stat text="Total: " amount={totalReviews} />
</div>
);
}
}
const App = () => {
const [good, setGood] = useState(0);
const [neutral, setNeutral] = useState(0);
const [bad, setBad] = useState(0);
let totalReviews = good + neutral + bad;
return (
<div>
<Stats totalReviews={totalReviews} />
</div>
)
}
I have used the debugger command to check in Chrome's developer console the values of each variable. It shows that totalReviews = 0. The variables good, neutral, and bad all also = 0.
I've also used console.log(totalReviews).
0 is displayed by the console.log. How come my program enters the second conditional as if totalReviews isn't 0?
if (totalReviews === 0)
You only wrap js statements in curly braces inside jsx, but your if statement is just regular js.
Problem with your if condition.
it should be if (totalReviews === 0) or if (totalReviews == 0) to avoid strongly type conversation check.
You have added {} inside if condtion which is not a stadard way

How can I give a key in JSX the value of a variable depending on conditions

I'm learning React by implementing a front-end interface for the note app API that I created. I have succeeded in having a list of all the note titles in my database appear. I want to be able to click on a title and have the note expand into the text of the note. The easiest way I've found for this is to give the "key" attribute of the 'li' as a variable and to also declare the same variable in the JSX { } object because they have the same name.
I've been looking for an answer for this for a few days and have been unable to find this exact problem. You can put a variable in a normal JSX expression but I need to do it on the 'li' which means technically in the HTML.
Here's some code to understand what I'm saying.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
//AXIOS CALL
setTitles(response.data[0]);
});
}, []);
//^^^^^add the array there to stop the response.data from repeating WAY TOO MANY TIMES
let listTitles = titles.map(titles => (
<li className="noteTitles" key={titles.title}>
{titles.title}
</li>
));
let showText = titles.map(titles => (
<li className="openText" key= {titles.text_entry}>
{titles.text_entry}
</li>
))
let openNote = () => {
setOpen(open => !open);
if (open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{showText}
</ul>
</div>
);
}
if (!open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listTitles}
</ul>
</div>
);
}
};
return { openNote };
};
export default NoteData;
That is the code I currently have. Here's showing a more simplified version of the openNote function that maybe makes more sense and shows what I'm trying to do:
VariableHere = "";
let openNote = () => {
setOpen(open => !open);
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
};
let listNotes = titles.map(titles => (
<li className="noteTitles" key={VariableHere}>
{VariableHere}
</li>
));
return (
<div>
<ul onClick={openNote}>
{listNotes}
</ul>
</div>
);
On click of each element there should be a switch of the key elements so if the element is 'open' the key variable and given variable in the JSX object should be mapped to titles.text_entry and on '(!open)' the key and JSX should be mapped to titles.title.
first of all, you're using a ternary in a weird way:
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
Ternaries are meant to be expressions whose value is conditional, but you're using it like a shorthand if/else. Try something like
VariableHere = open ? titles.text_entry : titles.title;
which is both shorter and more readable.
Second of all, keys in an array of elements are meant to help React determine which elements to update, if an item represents the same object, its key shouldn't change. In this case, regardless of what you're displaying, an item in the array represents the same note. Always using the title as the key should be fine provided items can't have the same title. If they can, use some sort of unique ID instead. If the order of the items doesn't change throughout the life of the component, using the array index as the key is fine.
Lastly, what you seem to want to do is called "conditional rendering". There are many ways to achieve this in react, one such way is to use the pre-cited ternary operator. Here is a minimal working example:
const listNotes = titles.map(note => (
<li className="noteTitles" key={note.title}>
{open ? note.title : note.text_entry}
</li>
));
const openNote = () => {
setOpen(!open);
}
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listNotes}
</ul>
</div>
)
You could also use a ternary in the key expression, but as I talked about above, it's not a good idea to do so.
Given your data-structure, I think you can simplify your code a bit. There is no need to create separate arrays for titles and contents. It sounds like you just want to expand and collapse a note when it is selected.
Here is a really simplified version on how you an do this. I'll use a sample data-set since we don't have access to your API.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [currentNote, setCurrentNote] = useState({});
useEffect(() => {
//AXIOS CALL
// setTitles(response.data[0]);
let data = [
{ id: 1, title: "a", text_entry: "what" },
{ id: 2, title: "b", text_entry: "is" },
{ id: 3, title: "c", text_entry: "up?" }
];
setTitles(data);
}, []);
const handleClick = noteId => {
let selectedTitle = titles.find(title => title.id == noteId);
//"collapse" if already selected
if (noteId === currentNote.id) {
setCurrentNote({});
} else {
setCurrentNote(selectedTitle);
}
};
let listTitles = titles.map(title => (
<li
className="noteTitles"
key={title.title}
onClick={() => handleClick(title.id)}
>
{title.title}
{title.id === currentNote.id && <div>{title.text_entry}</div>}
</li>
));
return (
<div>
Click on link item
<ul>{listTitles}</ul>
</div>
);
};
See working sandbox: https://codesandbox.io/s/old-silence-366ne
The main updates:
You don't need to have an "open" state. To be more succinct and
accurate, you should have a currentNote state instead, which is
set when clicking on a list item.
Have your handleClick function accept a noteId as an argument.
Then use that noteId to find the corresponding note in your titles
state. Set that found note as the currentNote. If the selected
note was already the currentNote, simply set currentNote to an
empty object {}, thus creating our expanding/collapsing effect.
In the JSX, after the title, use a ternary operator to conditionally
display the currentNote. If the note being mapped matches the
currentNote, then you would display a div containing the
text_entry.

Categories