I am having trouble passing data from component to main file in react. It is a basic quiz application, get quiz objects from api and pass data to the "Question" component. In the main file i have score state to decide how many answers are correct. But when answer button clicked application see value in the component. I want to match answer value and correct answer and increase score by state. Here is the Question component and main file.
export default function Question(props) {
const [flag,setFlag] = React.useState(true)
// TOGGLE BUTTON BACKGROUND COLOR WHEN BTN CLICKED
function toggleBtnBgColor(btn) {
if(flag) {
btn.target.classList.toggle("dark-bg-color")
setFlag(false)
}
}
return (
<>
<div className="question" key={props.index}>{props.question}</div>
{props.answers.map((answer,index)=> {
return (
<button key={index} onClick={toggleBtnBgColor} className="answer-button">{answer}</button>
)
})}
<hr></hr>
</>
)
}
Main file;
const [data, setData] = React.useState([])
const [score, setScore] = React.useState(0)
const [startAgain, setStartAgain] = React.useState(false)
// FETCH QUIZ DATA FROM API
const fetchData = async () => {
await fetch("https://opentdb.com/api.php?amount=5&type=multiple")
.then((response) => response.json())
.then((data) => setData(data.results))
}
React.useEffect(()=> {
fetchData()
},[])
// SHUFFLE ALGORITHM TO USE LATER
function shuffle(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
// RENDER QUESTIONS
const questionElements = data.map((question, index) => {
// ANSWERS ARRAY AND SHUFFLE
let answers = [...question.incorrect_answers,question.correct_answer]
shuffle(answers)
return (
<Question key={index} index={index} question={question.question} answers={answers}/>
)
})
// CALCULATE FINAL SCORE WHEN CHECK BTN CLICKED
function calcScore() {
for(let question of questionElements) {
console.log(question.calcScore)
}
setStartAgain(true)
}
return (
<div className="question-container">
{questionElements}
<p>{startAgain && ("Your score is "+ score +"/5")}</p>
<button onClick={calcScore} className="check-button">{!startAgain ? "Check Answers" : "Restart Quiz"}</button>
</div>
)
Check this out, we pass a function to Question that receives the answer and index. The parent will update an array that carries all the answers
https://codesandbox.io/s/black-cdn-p6lo5v?file=/src/App.js
const setAnswer = (index, answer) => {
const newAnswers = [...answers];
newAnswers[index] = answer;
setAnswers(newAnswers);
};
// RENDER QUESTIONS
const questionElements = data.map((question, index) => {
// ANSWERS ARRAY AND SHUFFLE
let answers = [...question.incorrect_answers, question.correct_answer];
shuffle(answers);
return (
<Question
key={index}
index={index}
question={question.question}
answers={answers}
setAnswer={setAnswer}
/>
);
});
If I am not wrong I understand that you want to update parent component's state. You can pass setState function as props to child component like this:
return (
<Question key={index} index={index}
question={question.question}
answers={answers}
updateScore ={setScore}/>
)
You can make use of props for the same and pass setScore hook to set the values.
You can make use of Redux and get/set the values to/from it.
Most people prefer redux over passing the setState hooks to children as props, however many times people pass the hook as props and solve this.
Hope this helps you!
Related
I have the following code in React. This code uses a time expensive function that performs some coputations over a large number of data. in the begining the state is filled with an array that says no computations. When the button is pressed, I want that array of the state to be filled with waiting, and after each iteration over i is complete, the element on potition i to be 'Done'. The problem is that by using set timeout, the DOM elements are not updating untill the whole function is over. Ignore the computations in the for, they are meaningless in this example. To have an ideea the function performs polynomial fit over the given data. My problem is how can I update the Dom elements while the function is running, So i can knwow which data is done and which is computing in real time
const Component = ( {data1, data2} ) => {
const [state, setState] = useState(Array(data1.length).fill('No Computations'))
const timeExpensiveFunction = () => {
// data1.length = 10
let variable0
let variable1
let variable2
let variable3
for(let i = 0; i< data1.length; i++){
//data1[i].length = 40 000
for(let j = 0; j< data1[i].length; j++){
variable1 += data1[i][j]
variable2 += variable1*data2[i][j]**3
variable3 += data2[i][j]**2
variable1 += data1[i][j]*data2[i][j]
}
setTimeout(() => {
setState(state => {
return[
...state.slice(0,i),
'Done',
...state.slice(i+1),
]
})
},1000)
}
return([variable1,variable2,variable3,variable4])
}
const randomFunc = (e) => {
setTimeout((e) => setState(Array(data1.length).fill('Waiting ...')),1000)
timeExpensiveFunction() //Duration depends on the number of data, it is usually higher that 10-15 seconds
}
return (
<div>
<div>
{state.map((element) => (
{element} //This does not update with the function setTimeout. It sets after the expensive computation is over
))}
</div>
<div>
<button onClick = {(e) => randomFunc(e)}>press</button>
</div>
</div>
)
}
First of all, You must use useState and useEffect to set a state and re-render the DOM.
You can use like below:
import { useState, useEffect } from 'react'
const Component = () => {
const [state, setState] = useState([1,2,3,4,5,6,7,8,9,10])
const [isPress, setPress] = useState(false); // use it to not call a method on first render
const timeExpensiveFunction = () => {
// lot of work here
}
const randomFunc = (e) => {
setState([10,9,8,7,6,5,4,3,2,1])
setPress(true)
}
useEffect({
if (isPress) setTimeout(() => timeExpensiveFunction(),1000)
}, [state])
return (
<div>
<div>
{state.map((element) => (
{element}
))}
</div>
<div>
<button onClick = {(e) => randomFunc(e)}>press</button>
</div>
</div>
)
}
I am experiencing a very odd issue with my react code : useState isn't updating the view and after literally trying everything the issue is still there. I made a simple code to explain the issue :
function(){
const [enterJob, setEnterJob] = useState(false);
const [jobSelection, setJobSelection] = useState(Array(someList.length).fill(false));
const jobRef = useRef();
const handleJobClick = i => {
const n = parseInt(i.target.id.charAt(0)); // the list is small enough to allow this
let c = jobSelection;
c[n] = !c[n];
setJobSelection(c);
};
const handleMouse = (e) =>{
if (!jobRef.current.contains(e.target)){
setEnterJob(false);
};
};
useEffect(() => {
window.addEventListener("mousedown", handleMouse);
return () => window.removeEventListener("mousedown", handleMouse);
});
return(
<div ref={jobRef}>
<input onFocus={()=> setEnterJob(true)} />
<div style={{display: `${enterJob ? 'flex' : 'none'}`}} >
<ul>
{ someList.map((item,index)=>
<li id={`${index}`} onClick={handleJobClick}> {jobSelection[index] ? item : "you clicked on the button"} </li> )}
</ul>
</div>
</div>
)
}
Some explanations: I am using UseEffect and useRef to create a dropDown menu that disappears when you clic outside the container. Now when I want to clic on a value of this drop-down menu it doesn't update the DOM while I am using useState to update the value of the string responsible for the change.
Thank you in advance,
Charbel
The problem is that you are mutatiing your jobSelection instead of creating a new object. And react will skip the rerender if the the objects has the same reference as before:
const handleJobClick = i => {
const n = parseInt(i.target.id.charAt(0)); // the list is small enough to allow this
let c = [...jobSelection]; // Create a new array
c[n] = !c[n];
setJobSelection(c);
};
Issues
If I understand your issue then I believe it is because you are directly mutating your state.
const handleJobClick = i => {
const n = parseInt(i.target.id.charAt(0)); // the list is small enough to allow this
let c = jobSelection;
c[n] = !c[n]; // <-- mutation!
setJobSelection(c);
};
You are also missing react keys on the mapped list items.
Solution
Since the next state depends on the previous state you should use a functional state update to copy your state first, then update it.
I suggest:
converting handleJobClick to consume the index directly, a curried function handles this cleanly
Add a react key to the mapped list items
Code
const handleJobClick = index => () => {
setJobSelection(jobSelection => jobSelection.map(
(selection, i) => index === i ? !selection : selection // <-- toggle selection at matched index
);
};
...
<ul>
{someList.map((item, index)=> (
<li
key={index} // <-- index as react key, ok since not adding/removing/sorting jobs
onClick={handleJobClick(index)} // <-- pass index to handler
>
{jobSelection[index] ? item : "you clicked on the button"}
</li>
))}
</ul>
I'm making a little game where the user guesses which of the two foods has the most calories. I scraped data on 400+ food items and imported in my component.
I want to make sure that both randomly selected food items do not have the same number of calories. I put a while loop in componentDidMount to check if it's true. If true, it will setState to another randomly selected food – repeat until they're no longer the same number.
For now, I have my while loop's condition set to '!==' for testing. This will be changed to '===' after I know it works the other way. The problem I'm having is that in my console, the foods will always be the same objects because setState isn't working for some reason.
import Foods from "../foods.json";
class Game extends Component {
state = {
firstFood: Foods[Math.floor(Math.random() * Foods.length)],
secondFood: Foods[Math.floor(Math.random() * Foods.length)],
mostCalories: "placeholder"
};
componentDidMount() {
while (
// to check that the two foods' calories are not the same
this.state.firstFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
console.log(this.state.firstFood.attributes.calories);
console.log(this.state.secondFood.attributes.calories);
debugger;
// an infinite loop because the below setState function doesn't run properly
this.setState({
firstFood: Foods[Math.floor(Math.random() * Foods.length)]
});
}
}
Another issue I'm facing is that in my debugger, when I try to manually enter Foods[Math.floor(Math.random() * Foods.length)], I get a "Food is not defined" although it works in the state initialization. However, when I enter _foods_json__WEBPACK_IMPORTED_MODULE_1__ in console from autofill, I have all of my data there so this works (replaced the word "Foods"):
_foods_json__WEBPACK_IMPORTED_MODULE_1__[Math.floor(Math.random() * _foods_json__WEBPACK_IMPORTED_MODULE_1__.length)]
I can't use that line of code in React, though.
http://www.giphy.com/gifs/UqwKkdF2ok2hlTwlLB
I would suggest slightly different approach for generating a pair of pseudo-random items, that may not save you CPU cycles, but will certainly avoid loops that led you to your current position.
So, the idea is rather simple:
you pick first random item
then you filter out the items with the same nutritional value
you pick another random item from remaining ones
pickTwo = arr => {
const firstItem = arr[0|Math.random()*arr.length],
filteredArr = arr.filter(({value}) => firstItem.value != value),
secondItem = filteredArr[0|Math.random()*filteredArr.length]
return [firstItem, secondItem]
}
You may find the demo of that concept below:
//dependencies
const { useState, useEffect } = React,
{ render } = ReactDOM
//sample data set
const data = [{name:'BBQ Ranch Burger',value:350},{name:'Big Mac',value:540},{name:'Double Cheesburger',value:440},{name:'Bacon Cheddar McChicken',value:540},{name:'Chicken McNuggets (10 piece)',value:440},{name:'Bacon Biscuit',value:350}]
//food card component
const FoodCard = ({name,value}) => (
<div style={{width:100,height:150,display:'table-cell', border:'1px solid black',verticalAlign:'middle',textAlign:'center'}}>{name}<br/>(calories: {value})</div>
)
//game board component
const GameBoard = () => {
//store foods within local state
const [foods,setFoods] = useState([]),
//describe thw way of picking two pseudo-random items
pickTwo = arr => {
const firstItem = arr[0|Math.random()*arr.length],
filteredArr = arr.filter(({value}) => firstItem.value != value),
secondItem = filteredArr[0|Math.random()*filteredArr.length]
return [firstItem, secondItem]
}
//pick two cards on initial render
useEffect(() => setFoods(pickTwo(data)),[])
//return component
return (
<div>
<FoodCard {...foods[0]} />
<FoodCard {...foods[1]} />
<button onClick={() => setFoods(pickTwo(data))}>reset</button>
</div>
)
}
render(
<GameBoard />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
The reason your loop will run endlessly is that, when you call setState the state is not actually immediately updated, there is a lag on it, which has to do with the React component lifecycle.
So instead of calling the setState in the loop. Declare a local variable, update the variable in the loop and when the loop is done, then set the state.
componentDidMount() {
let myFood = this.state.firstFood
while (
// to check that the two foods' calories are not the same
myFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
myFood = Foods[Math.floor(Math.random() * Foods.length)]
}
this.setState({
firstFood: myFood
});
}
I did not test this in detail but that should give you the idea.
State is not just some variable that you set whenever you want. It is reactive. It causes re-renders. You should pick two foods you want to have, make sure they don't have the same calories and only then do you setState them
There is no reason to setState on componentDidMount, your logic belongs in constructor
const _pickFoods = (allFoods) => {
const firstFood = allFoods[Math.floor(Math.random() * allFoods.length)]
let secondFood
do {
secondFood = allFoods[Math.floor(Math.random() * allFoods.length)]
} while (firstFood.attributes.calories !== secondFood.attributes.calories)
return { firstFood, secondFood }
}
class Game extends Component {
constructor(props) {
super(props)
const { firstFood, secondFood } = _pickFoods(Foods)
this.state = {
firstFood,
secondFood
}
}
...
}
That is if you want this while logic in your foods. It will loop forever if your Foods array consists of unviable foods only. Try this better alternative
const _pickFoods = (allFoods) => {
const firstFood = allFoods[Math.floor(Math.random() * allFoods.length)]
const possibleSecondFoods = allFoods.filter(food => food !== firstFood && food.attributes.calories !== firstFood.attributes.calories)
const secondFood = possibleSecondFoods[Math.floor(Math.random() * possibleSecondFoods.length)]
return secondFood
}
Try this:
import Foods from "../foods.json";
class Game extends Component {
state = {
firstFood: Foods[Math.floor(Math.random() * Foods.length)],
secondFood: Foods[Math.floor(Math.random() * Foods.length)],
mostCalories: "placeholder"
};
componentDidMount() {
setTimeout( () => {
while (
// to check that the two foods' calories are not the same
this.state.firstFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
console.log(this.state.firstFood.attributes.calories);
console.log(this.state.secondFood.attributes.calories);
debugger;
// an infinite loop because the below setState function doesn't run properly
this.setState({
firstFood: Foods[Math.floor(Math.random() * Foods.length)]
});
}
},2000)
}
I think it is because the data is not ready yet.
i wanted to remove an element from an array but i'm not getting, my code is:
const renderCount = state => {
const peopleHtml = state.filteredPeople.map(person => {
const personHtml = document.createElement('LI');
const personName = document.createTextNode(person.name);
const buttonDelete = document.createElement('Button');
const textOfButtonDelete = document.createTextNode('Delete');
buttonDelete.appendChild(textOfButtonDelete);
personHtml.appendChild(personName);
personHtml.appendChild(buttonDelete);
buttonDelete.onclick = function() {
return {...state,filteredPeople : state.filteredPeople.filter( (item, index) => index !=="Jean")}
}
return personHtml;
});
resultado.innerHTML = '';
peopleHtml.forEach(personHtml => resultado.appendChild(personHtml));
};
export default renderCount;
What the code makes?
He renders the elements of an array, 3 in 3. Each element of array have a button 'delete'and each time that i clicked that, a element get out off the screen.
The code and the button are: buttonDelete.onclick.....
Thanks and good afternoon.
This is not the right way to do in reactjs. React believe on Virtual DOM. Your states values and HTML elements will be talking with each other and you do not need to use appendChild or removeChild to update them , just update the state value. Some thing like below
render()
{
return (
<div>
<ul>
{this.state.filteredPeople.map(person => { =>
<li>
{person.name}
<button onClick={this.deleteme.bind(this,person.id)}>Delete</button>
</li>
)}
</ul>
</div>
);
}
deleteme(index){
let initial_val = this.state.contents.slice();
let obj_to_del= initial_val.find(person => person.id === id);
initial_val.remove(obj_to_del);
this.setState({people:initial_val});
}
EDIT:
It seems that this warning has something to do with my input range component. I will search for the problem and ask here, if I can`t find the problem. Thanx
I searched here and some other threads, but could`t find a solution.
I am creating a questionnaire with some dynamic pages, where I can answer radio buttons, checkboxes etc.
When I click the next button, current values will be stored and the new Page comes.
I realized it with a counter for the pages.
If the counter reaches the and of the Pages a result Page will be shown.
When I click the next buttons all seems to work, but I get the warning:
Warning: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I reduced the code for showing here. I hope this is ok.
this is the constructor:
constructor(props) {
super(props);
this.state = {
startDate: moment(),
values: {}
};
}
render function:
render() {
const {show_result} = this.props;
if(!show_result) {
return (
<div className="applicationbody">
{this.renderActiveForm()}
</div>
);
} else {
const {children} = this.props;
return (
<div className="applicationbody">
{children}
</div>
);
}
}
renderActivForm function:
renderActiveForm = () => {
const {page_now, count_cats, count_pages, categories_pages_quantity, language, catValuesCountAll, cat_values} = this.props;
return (
<div className="applicationbody__form">
{page_now.block.map((oneblock, i) => {
return (
this.renderReturnInput(oneblock, i)
);
})}
<div className="ButtonBar">
<a className="button"
onClick={this.save}>weiter</a>
</div>
</div>
);
}
save function:
save = () => {
const {page_now, count_cats, count_pages, categories_pages_quantity, catValuesCountAll} = this.props;
const cat_new = (count_pages + 1) === categories_pages_quantity.cats[count_cats].pages ? (count_cats + 1) : count_cats;
const page_new = (count_pages + 1) === categories_pages_quantity.cats[count_cats].pages ? 0 : (count_pages + 1);
if (page_now.evaluate === "1") {
var cat_value_of_this_page = sum(this.state.values);
function sum( obj ) {
var sum = 0;
for( var el in obj ) {
if( obj.hasOwnProperty( el ) ) {
sum += parseFloat( obj[el] );
}
}
return sum;
}
catValuesCountAll(page_new);
} else {
}
}
the catValuesCountAll action only increments the pagenow, so the new page with ne questions is shown. But why I get the warning?
I would be very happy if somebody could help me here.
EDIT:
It seems that this warning has something to do with my input range component. I will search for the problem and ask here, if I can`t find the problem. Thanx
Ok, I solved the problem now. The input-range component want to have a state for the min and max values.
So I setted the state during the rendering, cause it had to be dynamic. But now I set the state in componentWillMount and componentWillReceiveProps and the warning is gone.