I have a quiz and it has an arr of questions, a question has value.
I want to assign the total of questions question.value to the quiz.value as the value updates.
Here is how i am currently handling the updating (obviously i havent dealt with if a question is removed yet)
const handleUpdateQuestionScore = (e) => {
setQuiz({...quiz, questions: quiz.questions.map((child, index) => index === selectedQuestion ? { ...child, value: parseInt(e.target.value) } : child)})
setReUpdateTotal(true);
}
useEffect(() => { // this has weird behaviour
console.log("my total", quiz.questions.reduce((total, question) => total = total + question.value, 0))
if(quiz.questions.length < 1) {
setQuiz({ ...quiz, value: 0 } )
}
else{
setQuiz({...quiz, value: quiz.questions.reduce((total, question) => total = total + question.value, 0) } )
}
},[reUpdateTotal])
The issue with this is, when i set question 1, its correct.
When i add question 2 and set its value, sometimes it only interpolates the first number.
going back off the form and loading it up will update.
Other times it will not update the value at all.
For a much more indept demonstration, this codeSandbox demonstrates functionality:
https://codesandbox.io/s/stupefied-shadow-i9z2wx?file=/src/App.js
Here is the default code in the codeSandbox
import { useEffect, useState } from "react";
export default function App() {
const [quiz, setQuiz] = useState({
title: "",
number: 6,
questions: [
{
question: "q1",
value: 0
},
{
question: "q2",
value: 0
}
]
});
const [selectedQuestion, setSelectedQuestion] = useState(undefined);
useEffect(() => {
console.log("quiz", quiz);
}, [quiz]);
const [reUpdateTotal, setReUpdateTotal] = useState();
const handleUpdateQuestionScore = (e) => {
setQuiz({
...quiz,
questions: quiz.questions.map((child, index) =>
index === selectedQuestion
? { ...child, value: parseInt(e.target.value) }
: child
)
});
setReUpdateTotal(true);
};
useEffect(() => {
// this has weird behaviour
console.log(
"my total",
quiz.questions.reduce(
(total, question) => (total = total + question.value),
0
)
);
if (quiz.questions.length < 1) {
setQuiz({ ...quiz, value: 0 });
} else {
setQuiz({
...quiz,
value: quiz.questions.reduce(
(total, question) => (total = total + question.value),
0
)
});
}
}, [reUpdateTotal]);
return (
<div className="App">
<div>
{quiz.questions.map((question, index) => (
<div key={index}>
<p>
{question.question} - {question.value}
<button onClick={() => setSelectedQuestion(index)}>select</button>
</p>
<br />
</div>
))}
</div>
<br />
<small>change value for question #{selectedQuestion}</small>
{selectedQuestion !== undefined ? (
<div>
<input
type="number"
id="Value"
name="Value"
required="required"
className="edit-input shadow"
min="0"
value={quiz.questions[selectedQuestion].value}
onChange={(e) => {
handleUpdateQuestionScore(e);
}}
/>
<button onClick={() => setSelectedQuestion(undefined)}>remove</button>
</div>
) : (
<div>nothing selected</div>
)}
<h1>current total score = {quiz.value}</h1>
</div>
);
}
Technically - your useEffect that has a [reUpdateTotal] as a dependency array executes only once. When switching value from false to true. Im not seeing where you are switching it back. But still, you dont even need this variable and rely on the useEffect with [quiz] which will execute only 1 function - setQuiz. And no, it will not cause infinite rerenderings, if used correctly. It can accept a function with 1 parameter, which will be your "current" value of a quiz. I used prev but actually it is curr. And this function can return either a new value and fire a rerender, either the current value and do nothing in this case.
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [quiz, setQuiz] = useState({
title: "",
number: 6,
questions: [
{ question: "q1", value: 0 },
{ question: "q2", value: 0 }
]
});
const [selectedQuestionIndex, setSelectedQuestionIndex] = useState(undefined);
const handleUpdateQuestionScore = (e) => {
setQuiz((prev) => {
prev.questions[selectedQuestionIndex].value = parseInt(
e.target.value,
10
);
return { ...prev };
});
};
useEffect(() => {
setQuiz((prev) => {
const currValue = prev.value;
const newValue =
prev.questions?.reduce(
(total, question) => (total = total + question.value),
0
) || 0;
if (newValue === currValue) return prev;
return {
...prev,
value: newValue
};
});
}, [quiz]);
return (
<div className="App">
<div>
{quiz.questions?.map((question, index) => (
<div key={index}>
<p>
{question.question} - {question.value}
<button onClick={() => setSelectedQuestionIndex(index)}>
select
</button>
</p>
<br />
</div>
))}
</div>
<br />
<small>change value for question #{selectedQuestionIndex}</small>
{selectedQuestionIndex !== undefined ? (
<div>
<input
type="number"
id="Value"
name="Value"
required="required"
className="edit-input shadow"
min="0"
value={quiz.questions[selectedQuestionIndex].value}
onChange={handleUpdateQuestionScore}
/>
<button onClick={() => setSelectedQuestionIndex(undefined)}>
remove
</button>
</div>
) : (
<div>nothing selected</div>
)}
<h1>current total score = {quiz.value}</h1>
</div>
);
}
Here is a bit more clear way of handling "quiz.value" using useMemo:
import "./styles.css";
import { useMemo, useState } from "react";
export default function App() {
const [quiz, setQuiz] = useState({
title: "",
number: 6,
questions: [
{ question: "q1", value: 0 },
{ question: "q2", value: 0 }
]
});
const [selectedQuestionIndex, setSelectedQuestionIndex] = useState(undefined);
const selectedQuestion = useMemo(() => {
if (!quiz?.questions || selectedQuestionIndex === undefined)
return undefined;
return quiz.questions[selectedQuestionIndex];
}, [quiz, selectedQuestionIndex]);
const quizTotalValue = useMemo(() => {
if (!quiz || !quiz.questions) return 0;
return quiz.questions.reduce((acc, curr) => acc + curr.value, 0);
}, [quiz]);
const handleUpdateQuestionScore = (e) => {
if (!selectedQuestion) return;
selectedQuestion.value = parseInt(e.target.value, 10);
setQuiz((prev) => ({ ...prev }));
};
return (
<div className="App">
<div>
{quiz.questions?.map((question, index) => (
<div key={index}>
<p>
{question.question} - {question.value}
<button onClick={() => setSelectedQuestionIndex(index)}>
select
</button>
</p>
<br />
</div>
))}
</div>
<br />
<small>change value for question #{selectedQuestionIndex}</small>
{selectedQuestion !== undefined ? (
<div>
<input
type="number"
id="Value"
name="Value"
required="required"
className="edit-input shadow"
min="0"
value={selectedQuestion.value}
onChange={handleUpdateQuestionScore}
/>
<button onClick={() => setSelectedQuestionIndex(undefined)}>
remove
</button>
</div>
) : (
<div>nothing selected</div>
)}
<h1>current total score = {quizTotalValue}</h1>
</div>
);
}
Related
I have a list of products with different values.
const products = [{id: 2, value: 'A'}, {id:3, value: '3'}] // sample input
const RenderProduct = products.map((product) => {
return(
<div key={product.id}>
<MinusIcon onClick={SHOULD_DEDUCT_THE_QUANTITY}/>
<input type="text" value={SHOULD_DISPLAY_THE_QUANTITY_HERE} />
<AddIcon onClick={SHOULD_ADD_THE_QUANTITY}/>
</div>
)
});
return <RenderProduct />
How can I retrieve the current quantity of each product and display it on inputbox?
You can create a state using useState and based on the operation and id you can get the updated value
CODESANDBOX LINK
function Button({ onClick, operation }: any) {
return <button onClick={onClick}> {operation} </button>;
}
export default function App() {
const [products, setProducts] = useState([
{ id: 2, value: 2 },
{ id: 3, value: 3 }
]);
function handleChange(id: number, operation: "minus" | "add") {
setProducts((p) => {
return p.map((product) => {
if (product.id !== id) return product;
return {
...product,
value: operation === "minus" ? product.value - 1 : product.value + 1
};
});
});
}
return (
<>
{products.map((product) => {
return (
<div key={product.id}>
<Button
operation="-"
onClick={() => handleChange(product.id, "minus")}
/>
<input type="text" value={product.value} />
<Button
operation="+"
onClick={() => handleChange(product.id, "add")}
/>
</div>
);
})}
</>
);
}
I have a weird bug, where my code works on first attempt, but breaks on page re-render.
I've created a filter function using an object with filter names and array of filter values:
const filterOptions = {
'size': ['s', 'm', 'l'],
'color': ['black', 'white', 'pink', 'beige'],
'fit': ['relaxed fit','slim fit', 'skinny fit', 'oversize'],
'pattern': ['patterned', 'spotted', 'solid color'],
'material': ['wool', 'cotton', 'leather', 'denim', 'satin']
}
The idea was to create a separate object with all the values and corresponding 'checked' attribute and than use it to check if checkbox is checked:
const [checkedValue, setCheckedValue] = useState({})
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))}, [])
FilterValue here is array of values from FilterOptions:
<div className='popper'>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
There is onChange function as wel, which could be a part of problem:
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}}
I've tried keeping filterOptions in parent component and in Context, but it gives exactly the same result. It always work as planned on first render, and on next render it shows this error, until you delete the checked attribute of input. I've noticed that on re-render the 'checkedValue' object returns as empty, but I can't find out why. Would be really helpful if somebody could explain me a reason.
Uncaught TypeError: Cannot read properties of undefined (reading 'checked')
Edit: full code looks like this:
Parent Component
const Filter = () => {
return (
<div className='filter'>
<div className="price-filter">
<p>Price: </p>
<Slider onChange={handleSliderChange} value={[min, max]} valueLabelDisplay="on" disableSwap style={{width:"70%"}} min={0} max={250} />
</div>
<Divider />
<ul className='filter-list'>
{Object.entries(filterOptions).map((filter, i) => {
return (
<Fragment key={`${filter[0]}${i}`}>
<FilterOption className='filter-option' filterName={filter[0]} filterValue={filter[1]} />
<Divider key={`${i}${Math.random()}`} />
</Fragment>
)
})}
</ul>
</div>
)
}
Child Component
const FilterOption = ({ filterName, filterValue }) => {
const { checkedValue, setCheckedValue, activeFilters, setActiveFilters, filterOptions } = useContext(FilterContext)
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))
}, [])
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}
}
return (
<div className='popper' key={filterName}>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
)
Initially every items should active but When someone click on any items the others will disable/opacity(0.5). How to do that, I have tried a lot but can't find any solution.
const handleCheckbox = (e) => {
setActive(!active)
let selected = [...selectedHobby]
if (e.target.checked) {
selected = [...selectedHobby, e.target.value]
} else {
selected.splice(selectedHobby.indexOf(e.target.value), 1)
}
setSelectedHobby(selected)
router.push({
pathname: '',
query: { ...router.query, 'search': selected }
})
}
return (<>
<div className={`${active ? 'deactive' : 'active'}`}>
<input
type="checkbox"
name={props.name}
value={props.value}
onChange={(e) => handleCheckbox(e)}
/> {props.label}
</div>
</>
)
}
.deactive {
opacity: 0.50;
}
Sandbox:
https://codesandbox.io/s/aged-butterfly-ch7u13?file=/src/App.js
Here is the solution.
import "./styles.css";
import React, { useState } from "react";
let fruits = ["orange", "lemon", "apple", "watermelon"];
const Check = (props) => {
const handleCheckbox = (name) => {
const findIndex = props.selectedHobby.findIndex((v) => v === name);
if (findIndex === -1) {
return props.setSelectedHobby((array) => [...array, name]);
}
return props.setSelectedHobby((array) => array.filter((v) => v !== name));
};
return (
<>
<div
className={`${
props.selectedHobby.length === 0
? "active"
: props.selectedHobby.includes(props.name)
? "active"
: "deactive"
}`}
>
<input
type="checkbox"
name={props.name}
value={props.value}
onChange={(e) => handleCheckbox(props.name)}
/>{" "}
{props.label}
</div>
</>
);
};
const HobbyCheck = () => {
const [selectedHobby, setSelectedHobby] = useState([]);
return (
<>
{fruits.map((fruit, key) => (
<Check
selectedHobby={selectedHobby}
setSelectedHobby={setSelectedHobby}
key={key}
label={fruit}
name={fruit}
/>
))}
</>
);
};
export default HobbyCheck;
I have array of items and searching items function, that returns another array. When I delete or edit item finded items changes don't render, but when search string has another value React render changes.
I know that useEffect can resolve this problem, but dont what to put in callback.
How can resolve this problem?
export const ToDoList = (props: PropsType) => {
const [searchQuery, setSearchQuery] = useState('')
const searchedItems = useMemo(() => {
return props.ToDoData.filter(item => item.text.includes(searchQuery))
},
[searchQuery])
return (
{props.ToDoData.length ?
<>
<input
...
onChange={e => setSearchQuery(e.target.value)}
/>
<ItemsList
...
items={
searchQuery ?
searchedItems :
props.ToDoData
}
/>
</> :
...
}
)
}
export const ItemsList = (props: PropsType) => {
const [editedText, setEditedText] = useState('')
const onDeleteItem = (id: number) => {
props.dispatch(deleteItem(id))
},
onEditItemMode = (id: number, text: string) => {
props.dispatch(setEditMode(true, id))
setEditedText(text)
},
onEditText = (id: number) => {
props.dispatch(setEditedTextInItem(id, editedText))
props.dispatch(setEditMode(false, id))
setEditedText('')
},
onToggleCompletedStatus = (id: number, status: string) => {
...
}
return (
{props.items.length ?
props.items.map((object) => (
<div
className="Item"
key={object.id}
>
{props.inEditMode.some((id: number) => id === object.id) ?
<>
<input
value={editedText}
onChange={e => { setEditedText(e.currentTarget.value) }}
/>
<button onClick={() => onEditText(object.id)}>
Change text
</button>
</> :
<>
<div className="Item__textBlock">
<input
type='checkbox'
onClick={() => { onToggleCompletedStatus(object.id, object.status)}}
/>
<span className={
object.status === 'completed' ?
'completed' :
'in process'
}>
{object.text}
</span>
</div>
<div className="Item__buttonBlock">
<button
className="Item__button"
disabled={props.inEditMode.length !== 0}
onClick={() => onEditItemMode(object.id, object.text)}
>
<img src={editImg} />
</button>
<button
className="Item__button"
onClick={() => { onDeleteItem(object.id) }}
>
<img src={removeImg} />
</button>
</div>
</>
}
</div>
)) :
...
}
)
}
// This code creates a list that is ONLY updated when searchQuery is updated
const searchedItems = useMemo(() => {
return props.ToDoData.filter(item => item.text.includes(searchQuery))
}, [searchQuery]);
// This code creates the list every time the component renders,
// so it will always be correct
const searchedItems = props.ToDoData.filter(item => item.text.includes(searchQuery))
// If you absolutely need to optimize the render of this component
// This code will update the list whenever the reference for ToDoData is updated as well
const searchedItems = useMemo(() => {
return props.ToDoData.filter(item => item.text.includes(searchQuery))
}, [searchQuery, props.ToDoData]);
This should be some silly mistake I am doing, but I can't find the problem.
I am making an application that gets an array from the server, then, I display the received data filtered by the user input serch.
If only one country is found in the search, the information for that country is displayed.
if (filteredCountries.length === 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
}
If several countries are found (but less than 10), a list of them is displayed, with a button beside each country on the list, that if clicked, shows the information from that specific country, using the method handleClick.
if (filteredCountries.length > 0 && filteredCountries.length < 10 && filteredCountries.length > 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{<button onClick={handleClick}>show</button>}
</p>
));
}
handleClick should take as a prop the specific country, and display the data for that specific country. But it is not working for me.
const handleClick = (country) => {
console.log('click');
console.log(country);
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
};
What am I missing here?
Full code
Code sandbox
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const App = () => {
const [countries, setCountries] = useState([]);
const [searchFilter, setSearchFilter] = useState('');
const hook = () => {
console.log('effect');
axios.get('https://restcountries.eu/rest/v2/all').then((response) => {
console.log('promise fulfilled');
setCountries(response.data);
});
};
useEffect(hook, []);
console.log('render', countries.length, 'countries');
console.log(countries);
const handleClick = (country) => {
console.log('click');
console.log(country);
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
};
const filteredCountries =
searchFilter.length === 1
? countries
: countries.filter((country) => country.name.toLowerCase().indexOf(searchFilter.toLowerCase()) > -1);
const showCountries = () => {
if (filteredCountries.length > 10) {
return 'Too many matches, keep on typing';
}
if (filteredCountries.length > 0 && filteredCountries.length < 10 && filteredCountries.length > 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{<button onClick={handleClick}>show</button>}
</p>
));
}
if (filteredCountries.length === 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
}
};
const searchHandler = (e) => {
setSearchFilter(e.target.value);
};
return (
<div>
<div>
<h1>Countries</h1>
</div>
<div>
Type to find countries:
<input onChange={searchHandler} />
<div>{showCountries()}</div>
</div>
</div>
);
};
export default App;
You shouldn't return any view in the handle click. Instead, track the selected country on the click of show button and based on that selection render the country view.
Try this approach,
import React, { useState, useEffect } from "react";
import axios from "axios";
//setCountries is a function for setting the country's state
const App = () => {
const [countries, setCountries] = useState([]);
//Filter
const [searchFilter, setSearchFilter] = useState("");
const [selectedCountry, setSelectedCountry] = useState("");
const hook = () => {
console.log("effect");
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
console.log("promise fulfilled");
setCountries(response.data);
});
};
useEffect(hook, []);
/* by default the effect is always run after the component has been rendered. In our case, however, we only want to execute the effect along with the first render.
The second parameter of useEffect is used to specify how often the effect is run. If the second parameter is an empty array [], then the effect is only run along with the first render of the component. */
console.log("render", countries.length, "countries");
console.log(countries);
const renderCountryDetails = () => {
return (
selectedCountry && (
<p key={selectedCountry.alpha2Code}>
Capital: {selectedCountry.capital}. Population:{" "}
{selectedCountry.population} <img src={selectedCountry.flag} />
</p>
)
);
};
const filteredCountries =
searchFilter.length === 1
? countries
: countries.filter(
(country) =>
country.name.toLowerCase().indexOf(searchFilter.toLowerCase()) > -1
);
//showCountries returns either a message or else the contents of filteredcountries array
const showCountries = () => {
/* if (filteredCountries.length === 0) {
return 'No coincidences found'
} */
if (filteredCountries.length > 10) {
return "Too many matches, keep on typing";
}
if (
filteredCountries.length > 0 &&
filteredCountries.length < 10 &&
filteredCountries.length > 1
) {
return (
<div>
{filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{
<button onClick={() => setSelectedCountry(country)}>
show
</button>
}
</p>
))}
<div>{renderCountryDetails()}</div>
</div>
);
}
if (filteredCountries.length === 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population}{" "}
<img src={country.flag} />
</p>
));
}
};
const searchHandler = (e) => {
setSelectedCountry("");
setSearchFilter(e.target.value);
};
return (
<div>
<div>
<h1>Countries</h1>
</div>
<div>
Type to find countries:
<input onChange={searchHandler} />
<div>{showCountries()}</div>
</div>
</div>
);
};
export default App;
Codesandbox - https://codesandbox.io/s/kind-bohr-2crws?file=/src/App.js
You just need pass the country variable as a argument in button click same as below.
{<button onClick={() => handleClick(country)}>show</button>}
you also need to filter the country array in button click and need to set filtered array in state so that it will update and display.
I have updated your code hereUpdated Code.