How to change state in a React component? - javascript

I'm trying to get into details with React and make a simple Quiz app. I get some locally saved data and render it on the page. However, when I try to change state with data from true to false (toggle function), I get an error. Once it's triggered I get the following error: Cannot read properties of undefined (reading 'map'). Could you please let me know where the bug is? Thanks in advance.
const questBlocks = data.map((elem, index) => {
const ansBlocks = elem.answers.map((ans) => (
<Answer
toggle={toggle}
isSelected={ans.selected}
ans={ans.text}
key={ans.id}
id={ans.id}
/>
));
return (
<div key={index}>
<Question question={elem.question} />
<div className="answerCont">{ansBlocks}</div>
</div>
);
});
function toggle(id) {
setData((prevData) => {
return prevData.map((question) => {
return question.answers.map((answer) => {
return answer.id === id
? { ...answer, selected: !answer.selected }
: answer;
});
});
});
}```

A question appears to have an answers property, and probably has other properties too (such as the question text). When toggling an answer, you shouldn't change the question to be an array of answers, as your current code is doing - the question should remain an object with an answers property.
This
setData((prevData) => {
return prevData.map((question) => {
return question.answers.map((answer) => {
return answer.id === id
? { ...answer, selected: !answer.selected }
: answer;
});
});
});
should be
setData((prevData) => (
data.map((question) => ({
...question,
answers: question.answers.map((answer) => (
answer.id === id
? { ...answer, selected: !answer.selected }
: answer
))
}))
));
The callback for the state setter very likely isn't needed here either.
Also, if the questions have unique IDs, you should use those for keys instead of using indices.

Related

Make a new array and render it from map

So I've been stuck on this for a week now even though it's probably easy... I'm trying to make an array of objects using map over a hook. The final objective is to create a new hook from this new array and to be able to interact with it with some callbacks.
The issue is that when I try to just render it (before creating a hook from it), I have this error showing up :
Uncaught TypeError: Cannot read properties of undefined (reading 'value')
So here is my code :
import { useState } from 'react'
import { useEffect } from 'react'
import { nanoid } from 'nanoid'
import './App.css'
import FirstPage from './components/FirstPage'
import Quiz from './components/Quiz'
function App() {
const [startGame, setStartGame] = useState(false)
const [quizData, setQuizData] = useState([])
useEffect(()=>{
fetch(`https://opentdb.com/api.php?amount=5`)
.then (res => res.json())
.then (data => setQuizData(data.results))
}, [])
function gameStart(){
setStartGame(true)
}
const displayQuestions = quizData.map(({question, correct_answer, incorrect_answers})=>{
const answer=[]
const answers=[]
const questions ={
value : question,
id : nanoid()
}
const answer1={
text : incorrect_answers.slice(0,1),
is_selected : false,
is_correct : false,
id : nanoid()}
const answer2={
text : incorrect_answers.slice(1,2),
is_selected : false,
is_correct : false,
id : nanoid()}
const answer3={
text : correct_answer,
is_selected : false,
is_correct : true,
id : nanoid()}
const answer4={
text : incorrect_answers.slice(2),
is_selected : false,
is_correct : false,
id : nanoid()}
const answersOptions = answers.push(answer1,answer2,answer3,answer4)
const quiz = answer.push(questions, answers)
return <div>
{
answer.map(({questions, answers}) => (
<ul key={questions.id} >
<li className='questions'>{questions.value}</li>
<button className='answers'>{answers.text}</button>
</ul>
))
}
</div>
})
return (
<>
<h1 className='bloc'></h1>
<h1 className='bloc2'></h1>
{startGame
?
<Quiz displayQuestions={displayQuestions}/>
:
<FirstPage gameStart={gameStart}/>
}
</>
);
}
export default App
The main project is to make a quiz using some datas from an API.
Thank you for your help !
First of all, you can't just throw everything in the answer array and destruct it that way. Since you're getting the elements one by one from the quizData array anyway, you're only interested in a single question here, not questions. You must write the question on one line in the jsx you return and return the answers in the loop, otherwise you will have rewritten the same question for each answer.
const displayQuestions = quizData.map(
({ question, correct_answer, incorrect_answers }) => {
const answers = [
{
text: correct_answer,
is_selected: false,
is_correct: true,
id: nanoid()
},
...incorrect_answers.map((inc) => ({
text: inc,
is_selected: false,
is_correct: false,
id: nanoid()
}))
];
const q = {
value: question,
id: nanoid()
};
// to shuffle
let shuffledAnswers = answers
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
return (
<div>
<div key={q.id}>{q.value}</div> {/* question part */}
{shuffledAnswers.map(({ id, text, is_selected, is_correct }) => (
<ul key={id}>
<li className="answers">
{text}
</li>
</ul>
))}
</div>
);
}
);
The issue is that you are trying to push the questions and answers variables as items to the answer array, but in fact you need to create an object with the questions and answers properties and push to the array after.
Doing the way you did, the array would be equal to [questions, answers], but in fact you want it to be [{questions, answers}], then you could destruct the item and get the questions and answers properties.
So, in the line:
const quiz = answer.push(questions, answers);
It should be:
const quiz = answer.push({questions, answers});
But, the answer array is not necessary in this situation, since it's going to have just 1 item anyway.

how to setState update property of object instance in child list

I have an object quiz, it has a list of questions, questions have properties.
I want to update a property called question in object question (i know poor naming convention), inside questions without creating a separate instance of the selected question.
This is what I have so far (but it doesn't like the square bracket of selecting the particular question being edited):
onChange={(e) => setQuiz({ ...quiz, questions[quiz.questions.indexOf(selectedQuestion)].question: e.target.value })}
error:
Unexpected token, expected ","
img of syntax:
edit:
Here is a live demo example: https://codesandbox.io/s/recursing-dan-bv54nz?file=/src/App.js
I provide here a full working example and separate answer below incase this repo dies.
Full working code:
const [quiz, setQuiz] = useState({
title: "",
number: 6,
questions: [
{ question: "item1", number: 3 },
{ question: "item2", number: 3 }
]
});
const [selectedQuestion, setSelectedQuestion] = useState(0);
useEffect(() => {
setSelectedQuestion(0);
}, []);
useEffect(() => {
console.log("quiz", quiz);
console.log("quiz question", quiz.questions[0]);
}, [quiz]);
useEffect(() => {
console.log("selectedQuestion", selectedQuestion);
}, [selectedQuestion]);
return (
<div className="App">
<input
type="text"
value={quiz.questions[selectedQuestion].question}
onChange={(e) =>
setQuiz({
...quiz,
questions: quiz.questions.map((child, index) =>
index === selectedQuestion
? { ...child, question: e.target.value }
: child
)
})
}
/>
</div>
);
}
Super compact one-liner:
onChange={e => setQuiz({ ...quiz, questions: quiz.questions.map(q => q === selectedQuestion ? { ...q, question: e.target.value } : q) })}
This piece of code is really way too compact and hard to read.
So I personally would extract it to a named function instead of inline it. One more thing, I always prefer the setState(prevState => newState) updater pattern instead of setState(newState) directly. It avoids stale closure problem.
function MyComponent(props) {
const handleChange = (e) => {
setQuiz((quiz) => {
const questions = quiz.questions.map((q) => {
if (q !== selectedQuestion) return q;
return { ...q, question: e.target.value };
});
return { ...quiz, questions };
})
};
/* ... */
return <input onChange={handleChange} />
}
Can you check whether this one work or not? (I just tried to prevent from above errors)
onChange={(e) => {
const newQuestions = quiz.questions;
newQuestions[quiz.questions.indexOf(selectedQuestion)].question = e.target.value;
setQuiz({ ...quiz, questions: newQuestions });
}}
following #hackape & #Drew Reese suggestions I came up with the following:
The only real difference being also passing an index into the questions map so that it can correctly select the right element
onChange={(e) =>
setQuiz({
...quiz,
questions: quiz.questions.map((child, index) =>
index === selectedQuestion
? { ...child, question: e.target.value }
: child
)
})
}

How can I change State when an Event is triggered with useState?

to learn react im trying to implement a basic shop.
My Idea was to have many product-images. If an user clicks on an product-image this image turns around and shows something like comments, rating, etc of the product.
For this question i have 3 js Files:
Container.js (contains everything from the product-cards to navbar etc),
ProductList.js (returns the UL with all the different Products) and ItemCard.js (returns the actual product as LI ).
My Goal is to just invert the backsideVisible value.
I provide an minimal example for better understanding:
Container.js:
function Container() {
const [item, setItem] = useState([{
title: "some product title",
price: "14.99$",
backsideVisible: false
id: 1
}]);
function handleTurn(event, itemId) {
//here i want to change the backsideVisible value
event.preventDefault();
setItem(item.map(item => {
if(item.id === itemId) {
item.backsideVisible = !item.backsideVisible;
}
}))
}
return(
<ProductList items={item} handleTurn={handleTurn}/>
);
}
ProductList.js:
function ProductList(props) {
return(
<ul>
<CardItem items={props.items} handleTurn={props.handleTurn} />
</ul>
);
}
CardItem.js
function CardItem(props) {
return(
{props.items.map(item =>(
<li key={item.id} onClick={event => props.handleTurn(event, item.id)}>
product-image etc...
</li>
))}
);
}
But everytime i try this, ill get an "TypeError: can't access property "id", item is undefined" error.
As soon as i change the handleTurn Method to something like
function handleTurn(event, itemId) {
event.preventDefault();
console.log(itemId);
}
everything works fine and the console displays the id of the clicked item. So for me it seems, that my handleTurn Function has some errors.
Do you guys have any idea where my fault is?
Your help is much appreciated. Thank you.
You're setting item (which should really be called items since it's an array. names matter) to an array of undefined elements, because your map() callback doesn't return anything:
setItem(item.map(item => {
if(item.id === itemId) {
item.backsideVisible = !item.backsideVisible;
}
}))
Either return the updated object:
setItem(item.map(item => {
if(item.id === itemId) {
item.backsideVisible = !item.backsideVisible;
}
return item;
}))
or have the whole expression be a returned object:
setItem(item.map(item => ({
...item,
backsideVisible: item.id === itemId ? !item.backsideVisible : item.backsideVisible
})));

React Nested Map and Filter with conditional render does not work

I spent so much time on fixing this issue, i decided to ask people for help.
so basically what i am trying is to render a component.
but before rendering i have 2 arrays(labels, notes).
and using one of them(labels), i used map function and inside that i used filter function to get only element i want to pass to the component that is rendered.
I think there are some posts similar to this, but slightly different.
and some answers said, "when there is nested map function, the inner map needs to be wrap with tag like fragment" but what i found in other posts did not wrap it...
If anyone could help me clearing my confusion, would you please tell me if it is necessary or not.
Cannot Render Nested Maps In ReactJS <- says wrap with tag
React Nested map/forEach wont work <- does not use tag
here is my code.
let notes = [
{id:1, position:3, content: "apple"},
{id:2, position:2, content: "banana"},
{id:3, position:0, content: "orange"},
]
const labels = ["Discussion questions", "Quotes", "Sensory Ques", "Songs"];
const renderNotes = (notes) => {
return labels.map(label => {
console.log("---" + label + "---")
return (
notes.filter(note => {
// console.log(positionLabels[note.position] + " - " + label);
if (positionLabels[note.position] === label) {
console.log("BINGO");
return (
<CustomComponent
key={note.id}
content={note.content}
/>
)
}
})
)
})
}
and inside the return i am calling this function like below
{
renderNotes(notes)
}
when i was testing some like this
const renderNotes = (notes) => {
return positionLabels.map(label => {
return <div>
{
notes.filter(note => {
return (
<div>
TEST
</div>
)
})
}
</div>
})
}
it did not even work,
I have no idea what the error of objects are not valid as a react child is talking about.
First do filter then map it. Try like below
const renderNotes = (notes) => {
return labels.map(label => {
console.log("---" + label + "---")
return (
notes.filter(note => positionLabels[note.position] === label).map((value)=> {
console.log("BINGO");
return (
<CustomComponent
key={value.id}
content={value.content}
/>
)
})
)
})
}

ReactJS: Remove item from list without ID?

I have an issue I can't seem to crack even after checking out a few other posts on here and trying a few things out. I am playing around with React and making a quick todo list. Easy enough as I can add new names and display them on the page as intended. I want to be able to delete items that I choose and for that I was looking around and saw others doing something like this to delete items:
deleteName(id, e) {
const { names } = this.state;
this.setState({
names: names.filter(name => name.id !== id)
});
}
That made sense to me but I wasn't adding any id's to my <li> items so I thought I could just do:
this.setState({
names: names.filter(name => name !== name)
});
But this will just delete the whole list. What am I doing wrong? Should I restructure how I add names to the array to have an id and check that? I'll post the full component code below. Any help I can get is always appreciated. Thanks guys.
class ContactListPage extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
names: []
};
this.addName = this.addName.bind(this);
this.deleteName = this.deleteName.bind(this);
}
addName(name) {
const { names } = this.state;
if (name === '') {
console.log('add a name first!')
} else {
this.setState({
names: names.concat(name)
});
}
}
deleteName(id, e) {
const { names } = this.state;
this.setState({
names: names.filter(name => name !== name)
});
}
render() {
const { names } = this.state;
const named = names.map((name, i) => (
<Card key={i} className='todo-list'>
<CardText>
<li>{name}
<FloatingActionButton className='fab-delete' mini onClick={this.deleteName}>
<i className="material-icons fab-icon-delete" style={{color: 'white'}}>-</i>
</FloatingActionButton>
</li>
</CardText>
</Card>
));
return (
<div className='contact-list'>
<div className="field-line">
<NewName addName={this.addName} />
<ul className='new-name'>
{named}
</ul>
</div>
</div>
);
}
}
You're comparing each name with itself, that will yield an empty list regardless of what's in it (except I guess NaN?).
names.filter(name => name !== name)
I think you want to pass the name into your delete function from the view. It's been a while since I've done React, but you could probably do this with a lambda in the JSX.
deleteName(nameToDelete) {
const { names } = this.state;
this.setState({
names: names.filter(name => name !== nameToDelete)
});
}
render() {
// Simplified to focus on the onClick change
return names.map(name => <Card>
...
<FloatingActionButton onClick={(e) => this.deleteName(name)} ... />
...
</Card>);
}
If you need to worry about duplicate names, then you can pass the current index into deleteName() rather than the string itself. Up to you if that's necessary or not.
It's hard to tell, but are you referencing name when the passed argument to your callback is actually called id?
Try this:
deleteName(name) {
this.setState((prevState) => ({
names: prevState.names.filter(_name => name !== _name);
}));
}
And change the following:
onClick={this.deleteName.bind(null, name)}
In the callback for the filter, name !== name will always be false since an object is identical to itself. i.e name === name. Therefore the expression names.filter(name => name !== name) will return an empty array and erase your results. You can change your onClick handler to something like this to pass in your id: onClick={(e) => this.deleteName(i, e)} and then use names.filter((name, i) => i !== id) to 'delete' the name, although I imagine you would would probably want to make a copy of the names array and then splice the corresponding name out of it.

Categories

Resources