I have an issue with pushing an item to array inside the useState hook. When I try to push one element, it pushing twice. Code below:
export default function App() {
const [max, updateMax] = React.useState([]);
const [mid, updateMid] = React.useState([]);
const [low, updateLow] = React.useState([]);
const arr = [
{ letter: "A", number: 100 },
{ letter: "B", number: 80 },
{ letter: "C", number: 60 },
{ letter: "D", number: 40 }
];
useEffect(() => {
arr.map((element, index, array) => {
element.number === 100
? updateMax((prevState) => [...prevState, element.letter])
: element.number >= 60
? updateMid((prevState) => [...prevState, element.letter])
: element.number < 60
? updateLow((prevState) => [...prevState, element.letter])
: null;
});
}, []);
console.log(max, mid, low);
return <div className="App"></div>;
}
In console:
["A", "A"]
["B", "C", "B", "C"]
["D", "D"]
Expected output:
["A"]
["B", "C"]
["D"]
Why it's behaving like this?
The reason it runs twice is because you're running your app in Strict Mode.
Since React 18, developers of React decided to make useEffect() run twice when the app uses Strict Mode to help debugging useEffect(). If you get unexpected results from it running twice, it means that your code has some kind of smaller or bigger problem, either lack of proper cleanup code, or a little messy logic.
In your case, useEffect() is not the best fit for what you're trying to do. Try useMemo() instead.
It's likely this is happening due to the [...prevState,..] assignment. It could be possible that your useEffect fires twice, which in turn makes the function run twice.
One thing you can do to avoid duplicate items in your arrays, is to add a precondition to your update calls.
const isLetterAlreadyPresent = (arrayToCheck: string[], letterToCheck: string) => {
return arrayToCheck.some((letter) => letter === letterToCheck));
}
useEffect(() => {
arr.forEach((element, index, array) => {
if (element.number === 100 && !isLetterAlreadyPresent(max, element.letter) {
updateMax((prevState) => [...prevState, element.letter])
} else if (element.number >= 60 && !isLetterAlreadyPresent(mid, element.letter)){
updateMid((prevState) => [...prevState, element.letter])
} else if (element.number < 60 && !isLetterAlreadyPresent(low, element.letter)) {
updateLow((prevState) => [...prevState, element.letter])
}
});
}, []);
I also changed the map for a forEach, since you weren't using the result array that was given by the map, nor were you transforming anything within it. In order to iterate over an array, a forEach should suffice.
Related
I would like to count and show how much data are covered in each array.
At first, I filtered and set only the label which is not duplicated. And then I counted each as array. But it wouldn't work...
dataArray = ["a", "a", "bs", "bs", "bs", "bs", "vgvg"]
const [count, setCounts] = useState<any[]>([])
const [labels, setLabels] = useState<any[]>([])
useEffect(() => {
let b = dataArray.filter((x, i, self) => {
return self.indexOf(x) === i
})
setLabels(b)
console.log(label)
// ["a", "bs", "vgvg"]
}, [dataArray])
useEffect(() => {
let c = [] as any[]
dataArray.map((data) => {
c[data] = (c[data] || 0) + 1
})
setCounts(c)
console.log(count)
// {"a": 2, "bs": 4, "vgvg": 1}
}, [dataArray])
return (
{labels.map((label, idx) => (
<div>{label}: {counts[label]}</div>
))}
)
error
Element implicitly has an 'any' type because index expression is not of type 'number'.
You could probably use something like Array.prototype.reduce for this.
You could:
const TestComponent = ({ dataArray }) => {
// Memoise the counts so it doesn't need to be calculated on each render.
const counts = useMemo(() => {
// use array reduce to calculate counts.
return dataArray.reduce(
(state, item) => {
// if property does not yet exist add it and set to 0 count.
if (state[item] === undefined) {
state[item] = 0;
}
// increment count
state[item]++;
// return updated state
return state;
},
{} //initial state
);
}, [dataArray]);
return (
{
Object
// Use Object.keys to get an array of property names
.keys(counts)
// Map property names to desired view (label / count)
.map((key) => (
<div
key={key}
>
{key}: counts[key]
</div>
))
}
)
}
The example reduce function given a dataArray of:
const dataArray = ["a", "a", "bs", "bs", "bs", "bs", "vgvg"]
Would reduce to:
const result = {
a: 2,
bs: 4,
vgvg: 1
}
Essentially I have this state object and this method:
const [groupData, setGroupData] = useState({});
// groupData state
groupData = {
group1: [
{ id: 1, name: Mike, age: 24 },
{ id: 2, name: Bob, age: 31 }
],
group2: [
{ id: 3, name: Christin, age: 21 },
{ id: 4, name: Michelle, age: 33 }
],
}
const stateRef = useRef();
stateRef.current = groupData;
const handleRemovePerson = (personToRemoveById: string) => {
const filteredGroupData = Object.fromEntries(
Object.entries(groupData).map(([key, value]) => {
return [key, value.filter((person) => person.id !== personToRemoveById)];
}),
);
setMainContentData(filteredGroupData);
// Now check if group array does not have anymore persons, if empty then delete
group array
console.log('stateRef', stateRef);
// outputs the correct current data
const filteredStateRef = Object.keys(stateRef).map((key) => stateRef[key]);
console.log('filteredStateRef', filteredStateRef);
// outputs previous data ??
};
I tried useRef and once I loop through it, it gives me back the previous data. How can I get the most current data after setting state and then operating on that new data right away? Thank you!
First of all, you can't access the state after using setState because it's an asynchronous operation and if you want to check something after using setState you need use useEffect hook to listen for state change and decide what to do with it, or in recent versions of react and react-dom you could use a not so suggested function called flushSync which will would update the state synchronously.
so the prefered way is to use useEffect like this:
const handleRemovePerson = (personToRemoveById: string) => {
const filteredGroupData = Object.fromEntries(
Object.entries(groupData).map(([key, value]) => {
return [key, value.filter((person) => person.id !== personToRemoveById)];
}),
);
setMainContentData(filteredGroupData);
};
useEffect(() => {
if(true/**some conditions to prevents extra stuff */){
// do your things, for example:
console.log('stateRef', stateRef);
// outputs the correct current data
const filteredStateRef = Object.keys(stateRef).map((key) => stateRef[key]);
}
}, [mainContentData, /**or other dependencies based on your needs */])
I have set my initial state object like this:
const [Item,SetItem] = useState(
{
description: "Chicken",
foodNutrients:[
{nutrientName: "Protein", nutrientNumber: "203", value: 10},
{nutrientName: "Fat", nutrientNumber: "204", value: 15},
{nutrientName: "Carbs", nutrientNumber: "205", value: 20}
]
}
)
I want to modify my existing object by looping through foodNutrient array and doubling the value on button click, then storing the updated object in useState hook.
const Double = e => {
SetItem(Item.foodNutrients.forEach(foodNutrient => foodNutrient.value *= 2))
console.log(Item)
}
it logs the correct result to the console first time I try to click <button onClick={Double}>Set state</button>, but the second time it throws Cannot read property 'foodNutrients' of undefined and I cant render anything.
The logic in your event handler is replacing the initial object with undefined because Array.prototype.forEach returns undefined by default.
What you need to do is:
map to an array with new values
preserve the previous state and set array value in the state to the new maped array
const Double = (e) => {
const foodNutrientsNew = Item.foodNutrients.map((foodNutrient) => ({
...foodNutrient,
value: foodNutrient.value * 2,
}));
SetItem((prev) => ({ ...prev, foodNutrients: foodNutrientsNew }));
};
UPDATE
I would also suggest refactoring your code and splitting it into more components. Just follow the official docs article here
A much better approach is to do this in the onClick handler instead -
const Double = e => {
// clone your Item object
const cloneItem = {...Item};
// now make modifications in the new object
cloneItem.foodNutrients.forEach(foodNutrient => foodNutrient.value *=2 );
SetItem(cloneItem);
// to see the difference in console
console.log(cloneItem)
console.log(Item)
}
I tried a lot of things to help you and i came up with this solution which is working for me.
function App() {
const [item, setItem] = useState({
description: "Chicken",
foodNutrients: [
{ nutrientName: "Protein", nutrientNumber: "203", value: 10 },
{ nutrientName: "Fat", nutrientNumber: "204", value: 15 },
{ nutrientName: "Carbs", nutrientNumber: "205", value: 20 },
],
});
console.log(item);
const double = (e) => {
setItem({...item}, item.foodNutrients.map(e => e.value *=2));
}
return <button onClick={double}>Set state</button>;
}
Hi i have a react component expenses-total.js and a corresponding test case expenses-total.test.js as shown below.
expenses-total.js
export default (expenses=[]) => {
if (expenses.length === 0) {
return 0;
} else {
return expenses
.map(expense => expense.amount)
.reduce((sum, val) => sum + val, 0);
}
};
expenses-total.test.js
import selectExpensesTotal from '../../selectors/expenses-total';
const expenses = [
{
id: "1",
description: "gum",
amount: 321,
createdAt: 1000,
note: ""
},
{
id: "2",
description: "rent",
amount: 3212,
createdAt: 4000,
note: ""
},
{
id: "3",
description: "Coffee",
amount: 3214,
createdAt: 5000,
note: ""
}
];
test('Should return 0 if no expenses', ()=>{
const res = selectExpensesTotal([]);
expect(res).toBe(0);
});
test('Should correctly add up a single expense', ()=>{
const res = selectExpensesTotal(expenses[0]);
expect(res).toBe(321);
});
test('Should correctly add up multiple expenses',()=>{
const res = selectExpensesTotal(expenses);
expect(res).toBe(6747);
});
when i run the test case, its getting failed by giving an error
TypeError: expenses.map is not a function
I know the test case is correct but dont know what is wrong with thecomponent.
Could anyone please help me in fixing this error?
The problem is with if (expenses.length === 0) and the test case that uses selectExpensesTotal(expenses[0]):
expenses[0] passes an object, which has no length property, so in the function being tested, expenses.length returns undefined. However, undefined === 0 evaluates to false so your code goes into the else block tries to use .map on the object, which doesn't have that function, thus it throws an error.
In a brief: you can't map over an object.
expenses is an array of objects, so expenses[0] is an object.
Condition expenses.length === 0 evaluates to false, since obviously .length property does not exist on Object.prototype, so the else condition takes place - your function tries to map over an object.
The problem is that expenses[0] is an object (you probably expected it to be an array) and an object does not have a map function. A quick hack would be to add another ifs into the loop to check if expenses is actually an object. So that:
export default (expenses=[]) => {
if (expenses.length === 0) {
return 0;
} else {
if (typeof expenses === 'object') {
return expenses.amount
} else {
return expenses
.map(expense => expense.amount)
.reduce((sum, val) => sum + val, 0);
}
}
};
I hope this help.
To fix this error, you can pass in an array of object into
selectExpensesTotal([expenses[0]])
rather than just an object
selectExpensesTotal(expenses[0])
So your code show look like this:
test('Should correctly add up a single expense', ()=>{
const res = selectExpensesTotal([expenses[0]]);
expect(res).toBe(321);
});
.map function will now work on expenses. Because, this is now an array of object ( works with map function ) and not an object(This does not work with map function)
I use vue-multiselect to let user select items with filtering options:
query = "what user type in input field"
allLinks.filter(l=>l.labelName.includes(query))
and it works, but now I would like to extend the filtering to all properties of my object with this structure :
{
"labelName":"LK000056",
"extPort":{
"aPort":"EXTA-EQ001/board10port02",
"zPort":"EXTZ-EQ012/board09port02"
}
}
I would like with one query to get parent object if query match on labelName, aPort or zPort.
it is possible ? or maybe with a conditional way like :
allLinks.filter(l=>if(!l.labelName.includes(query)){l.extport.aPort.includes(query)}else{l.extport.zPort.includes(query)})
thank for helping me
forgive my approximate English I am French
You could recursively flatten the objects to array of strings, then search in them via Array.filter, Array.some & Array.includes:
const data = [{ "labelName":"LK000056", "extPort":{ "aPort":"EXTA-EQ001/board10port02", "zPort":"EXTZ-EQ012/board09port02" } }, { "labelName":"LK000057", "extPort":{ "aPort":"EXTA-EQ001/board123", "zPort":"EXTZ-EQ012/board333" } }]
const flatten = (obj, a=[]) => Object.values(obj)
.reduce((r,c) => (typeof c == 'object' ? flatten(c,a) : r.push(c), r), a)
const search = (d, t) =>
d.filter(x => flatten(x).some(x => x.toLowerCase().includes(t.toLowerCase())))
console.log(search(data, 'board123'))
console.log(search(data, 'LK000056'))
console.log(search(data, 'EXTZ-EQ012'))
Note that this is a generic approach and will work regardless of the nested levels of data, for example:
const data = [{
"A": {
"B": {
"C": {
"data": '11'
},
}
}
}, {
"D": {
"E": {
"data": '22'
}
}
}, {
"F": "33"
}]
const flatten = (obj, a = []) => Object.values(obj)
.reduce((r, c) => (typeof c == 'object' ? flatten(c, a) : r.push(c), r), a)
const search = (d, t) => d.filter(x =>
flatten(x).some(x => x.toLowerCase().includes(t.toLowerCase())))
console.log(search(data, '22'))
console.log(search(data, '11'))
console.log(search(data, '33'))
You can use Object.values to get the values of the object as an array, then use some to test if one or more items match.
const items = [{
"labelName": "LK000056",
"extPort": {
"aPort": "EXTA-EQ001/board10port02",
"zPort": "EXTZ-EQ012/board09port02"
}
}, {
"labelName": "234234",
"extPort": {
"aPort": "abc123",
"zPort": "1234567890"
}
}]
function search(query, data) {
return data.filter(i => {
if (i.labelName.includes(query)) return true
if (Object.values(i.extPort).some(v => v.includes(query))) return true
return false
})
}
console.log(search("EQ001", items))
console.log(search("1234567890", items))
console.log(search("lalalala", items))
if it can help I think found a functional solution let me know if something better can be done ...
allLinks.filter(l=>{
if (l.sdhPort.zSDHPort!=undefined && l.extPort.aPort.toUpperCase().includes(query.toUpperCase())){return true}
if (l.extport.zPort!=undefined && l.extPort.zPort.toUpperCase().includes(query.toUpperCase())){return true}
if (l.labelName!=undefined && l.labelName.toUpperCase().includes(query.toUpperCase())){return true}
})
wow thanks both of you !
it seems really interesting this technic ... but let me some time to understand it
what do think about what I found ?
best regards