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>;
}
Related
I am using the NHL api to try to grab players stats for a given season. I have a utility function with these season values :
export const seasonOptions = [
{ value: "19861987", label: "1986/1987" },
{ value: "19871988", label: "1987/1988" },
{ value: "19881989", label: "1988/1989" },
{ value: "19891990", label: "1989/1990" },
{ value: "19901991", label: "1990/1991" },
{ value: "19911992", label: "1991/1992" },
{ value: "19921993", label: "1992/1993" },
{ value: "19931994", label: "1993/1994" },
{ value: "19941995", label: "1994/1995" },
{ value: "19951996", label: "1995/1996" },
];
... and so on. In my component I have this state to setSelect on what was selected:
const [select, setSelect] = useState(seasonOptions[seasonOptions.length - 1]);
const handler = (selected) => {
setSelect((select) => select);
handlePlayerStats(
`https://statsapi.web.nhl.com/api/v1/people/${props.playerId}/stats?stats=statsSingleSeason&season=${selected.value}`
);
}
};
<Select
id="select"
instanceId={"select"}
options={seasonOptions}
placeholder={select.label}
autoFocus
value={select.value}
onChange={handler}
/>
Which calls this custom hook:
const handlePlayerStats = async (url) => {
try {
const req = await fetch(url).then((response) => response.json());
console.log(req);
if (req.messageNumber) {
setFetchedData([]);
} else if (req.stats[0].splits.length > 0) {
setFetchedData(req);
}
} catch (error) {
console.log(error);
}
};
I'm not really sure how to go about looping through all the seasonOptions dynamically and filtering out each season where req.stats[0].splits.length === 0?
Here is the codesandbox link for anyone curious: https://codesandbox.io/s/friendly-kapitsa-c97rzy?file=/components/PlayerStats.js:357-855
To Answer The first parst of your question you can map over this Array of Objects with this method for example using the .map method
React Code SandBox
MDN-.map() method JS
export const seasonOptions = [
{ value: 8471214, label: "198601987" },
{ value: 8471215, label: "198701988" },
{ value: 8471216, label: "198801989" },
{ value: 8471217, label: "198901990" },
{ value: 8471218, label: "199001991" },
{ value: 8471219, label: "199101992" },
{ value: 8471220, label: "199201993" },
{ value: 8471221, label: "199301994" },
{ value: 8471222, label: "199401995" },
{ value: 8471223, label: "199501996" },
];
//MAKE SURE TO MAP OVER THE <option></option> tag not the <Select/>
//CHECK CODE SANDBOX FOR EXAMPLE IN PURE HTML5 AND REACT
{seasonOptions.map((option,index)=>
<Select
key={index}
id="select"
instanceId={"select"}
options={option?.value}
placeholder={option?.label}
autoFocus
value={select.value}
onChange={handler}
/>
)}
Check Out my Answer Here or for other examples here how to map over an array and access it values this method is just 1 of many .
How to find a value from array object in JavaScript? Stack Over Flow Question
For the Second Part Of the Question you can use the new SandBox
Steps
Change the value property here from a string to a number by removing the quotation marks export const seasonOptions = [{value: 8471214, label: "198601987"},]
Assign a useState hook to handle the active filtered item
const [activeFilter, setActiveFilter] = useState([]);
3.Assign an arrow function to handle Filtering the Seasons
using the setTimeOut() method setTimeout()-MDN-DOCS Where 500 is the time the function is executed for that time and
const handleSeasonsFilter = (item) => {
setActiveFilter(item);
setTimeout(() => {
if (!item) {
setFilterSeasons(item);
} else {
setFilterSeasons(
seasonOptions.filter((season) =>
seasonOptions?.includes(season?.label)
)
);
}
}, 500);
};
Pass that to url that searches the API url = `https://statsapi.web.nhl.com/api/v1/people/${activeFilter}/stats?stats=statsSingleSeason&season=200402005 Like in Line 65 in The Sand Box
Display Them using useEffect() hook also add in the values in the dependency array or leave it empty to avoid infinite loops.
useEffect(() => {
//debug data
console.log(stringDataDisplay);
setActiveFilter(activeFilter);
//DEBUG IF VALUE IF PASSED
//setDatasearched(activeFilter);
}, [
/**EMPTY DEPENDENCY ARRAY TO AVOID INFINITE LOOPS */
stringDataDisplay,
activeFilter
]);
in My Example i Displayed Them using another useState Hook and State action
const [datasearched, setDatasearched] = useState([])
& Finally Just Assigned a new const stringDataDisplay = JSON.stringify([datasearched]);
To Stringify the [datasearched] Array Here.
Note
Make sure to pass the handleSeasonsFilter to OnClick as an empty arrow function and pass the option.value property as a String so the API Accepts the request.
Hope this helps with your Example and Ill try to Check the code sandbox also with your method.
Bear in Mind i still i am developing this and i understand you want the values of the seasons to be shown when no player id is selected Am i correct?
I have this state
this.state = {
dropdown1: false,
dropdown2: false,
dropdown3: false
}
I want to access to these dropdowns in state using this.setState but the number after 'dropdown' comes from API
onMaca = (ev) => {
this.setState({
dropdown + ev: true
})
}
So I want the key to be dynamic 'dropdown1' for example.
Thanks for your answers
you can access the object property like this object['property name']
onMaca = (ev) => {
this.state['dropdown' + ev]= true;
this.setState({
...this.state
})
}
https://codezup.com/add-dynamic-key-to-object-property-in-javascript/
You can use any of these to set key dynamically. I will try to update the answer with an example in a while for setState.
The state is a JS object, so you can get its keys as usual, like so:
const stateKeys = this.state.keys()
Now you have an array: [ "dropdown1", "dropdown1", "dropdown1" ]
One way to use it would be:
const keysMap = statekeys.map(( item, i ) => return {
key: item,
idx: i,
number: item.replace( /dropdown/, '' )
}
keysMap will look like so: [ { key: 'dropdown1', idx: 0, number "1" }, { key: 'dropdown1', idx: 1, number "2" }, { key: 'dropdown1', idx: 2, number "3" } ]
You can query keysMap for a given dropDownNumber like so:
let k = keysMap.find( kmap => kmap.key = dropDownNumber )
To set the dropdown's state:
this.setState({ k: <whatever> })
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 am building a React app that includes one separate component for CRUD functionality of Products and another separate component for CRUD functionality of Suppliers.
I am using the same saveData method for both components (the Create functionality of CRUD.. that is triggered when the User presses Save after filling in the input fields of Product or Supplier). The saveData method is located in a central ProductsAndSuppliers.js file that is available to both the Products and Supplier components.
In both of the Product & Supplier components, there is a table showing the Products or Suppliers already present as dummy data.
I made a button at the bottom of each page to add a new Product or Supplier... depending on which tab the user has selected on the left side of the screen (Product or Supplier).
Since I am using the same saveData method in both cases, I have the same problem whenever I try to add a new Product or Supplier to each respective table after filling out the input fields. My new Product or Supplier is added.. but twice and I can't figure out why.
I have tried using a spread operator to add the new item to the collection but am having no success:
saveData = (collection, item) => {
if (item.id === "") {
item.id = this.idCounter++;
this.setState((collection) => {
return { ...collection, item }
})
} else {
this.setState(state => state[collection]
= state[collection].map(stored =>
stored.id === item.id ? item : stored))
}
}
Here is my original saveData method that adds the new Product or Supplier, but twice:
saveData = (collection, item) => {
if (item.id === "") {
item.id = this.idCounter++;
this.setState(state => state[collection]
= state[collection].concat(item));
} else {
this.setState(state => state[collection]
= state[collection].map(stored =>
stored.id === item.id ? item : stored))
}
}
my state looks like this:
this.state = {
products: [
{ id: 1, name: "Kayak", category: "Watersports", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer", price: 19.50 },
],
suppliers: [
{ id: 1, name: "Surf Dudes", city: "San Jose", products: [1, 2] },
{ id: 2, name: "Field Supplies", city: "New York", products: [3] },
]
}
There are issues with both of your implementations.
Starting with the top one:
// don't do this
this.setState((collection) => {
return { ...collection, item }
})
In this case, collection is your component state and you're adding a property called item to it. You're going to get this as a result:
{
products: [],
suppliers: [],
item: item
}
The correct way to do this with the spread operator is to return an object that represents the state update. You can use a computed property name to target the appropriate collection:
this.setState((state) => ({
[collection]: [...state[collection], item]
})
)
* Note that both this and the example below are using the implicit return feature of arrow functions. Note the parens around the object.
In the second code sample you're
mutating the existing state directly which you should not do.
returning an array instead of a state update object.
// don't do this
this.setState(state =>
state[collection] = state[collection].concat(item)
);
Assignment expressions return the assigned value, so this code returns an array instead of an object and I'd frankly be surprised if this worked at all.
The correct implementation is the same as above except it uses concat instead of spread to create the new array:
this.setState(state => ({
[collection]: state[collection].concat(item)
})
);
needlessly fancy, arguably silly id generators:
const nextId = (function idGen (start = 100) {
let current = start;
return () => current++;
})(100);
console.log(nextId()); // 100
console.log(nextId()); // 101
console.log(nextId()); // 102
// ----------------
// a literal generator, just for fun
const ids = (function* IdGenerator(start = 300) {
let id = start;
while (true) {
yield id++;
}
})();
console.log(ids.next().value); // 300
console.log(ids.next().value); // 301
console.log(ids.next().value); // 302
I am deleting an one id in an array, how do I setState after filtering it here?
https://codesandbox.io/s/react-example-1m2qn
const Debtors = () => {
const debtors = [
{
id: 1,
name: "John",
relation: "friend",
statement: [
{ id: 1, date: 2010, amount: "1000", purpose: "John" },
{ id: 2, date: 2014, amount: "2000", purpose: "john" }
]
},
,
{
id: 2,
name: "Jack",
relation: "Friend",
statement: [
{ id: 1, date: 2010, amount: "1000", purpose: "jack" },
{ id: 2, date: 2014, amount: "2000", purpose: "jack" }
]
}
];
const [newDebtors, setdebtors] = React.useState(debtors);
const handleDelete = (stat, i) => {
const newList = newDebtors[0].statement.filter(x => x.id !== stat.id);
// How to set debtors here ?
// setdebtors({ ...newDebtors, statement[0]: newList });
console.log(newList)
// How to set debtors here ?
There's two problems:
1) You are iterating off the original debtors object in your render, instead of the newDebtors state you created via useState(), which is why there does not appear to be any UI change.
You need: newDebtors[0].statement.map
2) You need to pass in the item index in your handleDelete() so it knows what item in the array to update. You can have the function do something like this:
In the onClick:
<a
href="javascript:;"
onClick={() => handleDelete(stat, i, 0)}
>
In the handleDelete():
const handleDelete = (stat, i, arrayIndex) => {
const updatedDebtors = newDebtors.map((item, index) => {
if (index === arrayIndex) {
return {
...item,
statement: item.statement.filter(
statement => statement.id !== stat.id
)
};
} else {
return item;
}
});
setDebtors(updatedDebtors);
};
See sandbox for full solution: https://codesandbox.io/s/react-example-x7uoh
You should do it like that:
setdebtors((prevState) => {
let newArray = Array.from(prevState); // Copy the array
// Manipulate the array as you wish
return newArray; // return it
});
The problem is you are mutating the array of "debtors" you need to map through the array of debtors and change any properties in the object.
const handleDelete = (stat, i) => {
const newList = newDebtors.map((debtor, i) => {
if (i === 0) {
debtor.statement = debtor.statement.filter(x => x.id !== stat.id);
}
return debtor;
});
setdebtors(newList);};
An even better approach is to use "useReducer" which is used for mutating more complex pieces of state, like you have here. THe docs are very helpful useReducer
Hmm I dont know what exactly you are trying to do,
Is this what you are looking for?
const handleDelete = (stat, i) => {
const newList = newDebtors[0].statement.filter(x => x.id !== stat.id);
const newFirstItem = {...newDebtors[0],statement: newList}
const newDebtorList = newDebtors.filter(x => x.id !== newFirstItem.id);
newDebtorList.unshift(newFirstItem);
setdebtors(newDebtorList);
}
I know this seems complex but you kinda actually need to do this as you cannot mutate an array in the state...
What I did here is I first created a new statement list(newList), then created a newFirstItem to be set as the new newDebtors[0], then created a new array(newDebtorList) of all the elements of newDebtors except the first one, I modified this array by pushing the newFirstItem to the 0th position(using unshift)
Finally updated the state with this new array...
hope it helps :)
Note: this is for changing the 0th element if you have the id please change the code accordingly