Related
I have retrieved data stored using useState in an array of object, the data was then outputted into form fields. And now I want to be able to update the fields (state) as I type.
I have seen examples on people updating the state for property in array, but never for state in an array of object, so I don't know how to do it. I've got the index of the object passed to the callback function but I didn't know how to update the state using it.
// sample data structure
const data = [
{
id: 1,
name: 'john',
gender: 'm'
}
{
id: 2,
name: 'mary',
gender: 'f'
}
]
const [data, setData] = useState([]);
const updateFieldChanged = index => e => {
console.log('index: ' + index);
console.log('property name: '+ e.target.name);
setData() // ??
}
return (
<React.Fragment>
{data.map((data, index) => {
<li key={data.name}>
<input type="text" name="name" value={data.name} onChange={updateFieldChanged(index)} />
</li>
})}
</React.Fragment>
)
Here is how you do it:
// sample data structure
/* const data = [
{
id: 1,
name: 'john',
gender: 'm'
}
{
id: 2,
name: 'mary',
gender: 'f'
}
] */ // make sure to set the default value in the useState call (I already fixed it)
const [data, setData] = useState([
{
id: 1,
name: 'john',
gender: 'm'
}
{
id: 2,
name: 'mary',
gender: 'f'
}
]);
const updateFieldChanged = index => e => {
console.log('index: ' + index);
console.log('property name: '+ e.target.name);
let newArr = [...data]; // copying the old datas array
// a deep copy is not needed as we are overriding the whole object below, and not setting a property of it. this does not mutate the state.
newArr[index] = e.target.value; // replace e.target.value with whatever you want to change it to
setData(newArr);
}
return (
<React.Fragment>
{data.map((datum, index) => {
<li key={datum.name}>
<input type="text" name="name" value={datum.name} onChange={updateFieldChanged(index)} />
</li>
})}
</React.Fragment>
)
The accepted answer leads the developer into significant risk that they will mutate the source sequence, as witnessed in comments:
let newArr = [...data];
// oops! newArr[index] is in both newArr and data
// this might cause nasty bugs in React.
newArr[index][propertyName] = e.target.value;
This will mean that, in some cases, React does not pick up and render the changes.
The idiomatic way of doing this is by mapping your old array into a new one, swapping what you want to change for an updated item along the way.
setData(
data.map(item =>
item.id === index
? {...item, someProp : "changed", someOtherProp: 42}
: item
))
setDatas(datas=>({
...datas,
[index]: e.target.value
}))
with index being the target position and e.target.value the new value
You don't even need to be using the index ( except for the key if you want ) nor copying the old datas array,and can even do it inline or just pass data as an argument if you prefer updateFieldChanged to not be inline. It's done very quickly that way :
const initial_data = [
{
id: 1,
name: "john",
gender: "m",
},
{
id: 2,
name: "mary",
gender: "f",
},
];
const [datas, setDatas] = useState(initial_data);
return (
<div>
{datas.map((data, index) => (
<li key={index}>
<input
type="text"
value={data.name}
onChange={(e) => {
data.name = e.target.value;
setDatas([...datas]);
}}
/>
</li>
))}
</div>
);
};
This is what I do:
const [datas, setDatas] = useState([
{
id: 1,
name: "john",
gender: "m",
},
{
id: 2,
name: "mary",
gender: "f",
},
]);
const updateFieldChanged = (name, index) => (event) => {
let newArr = datas.map((item, i) => {
if (index == i) {
return { ...item, [name]: event.target.value };
} else {
return item;
}
});
setDatas(newArr);
};
return (
<React.Fragment>
{datas.map((data, index) => {
<li key={data.name}>
<input
type="text"
name="name"
value={data.name}
onChange={updateFieldChanged("name", index)}
/>
</li>;
<li key={data.gender}>
<input
type="text"
name="gender"
value={data.gender}
onChange={updateFieldChanged("gender", index)}
/>
</li>;
})}
</React.Fragment>
);
Spread the array before that. As you cannot update the hook directly without using the method returned by useState
const newState = [...originalState]
newState[index] = newValue
setOriginalState(newState)
This will modify the value of the state and update the useState hook if its an array of string.
const updateFieldChanged = index => e => {
name=e.target.name //key
let newArr = [...data]; // copying the old datas array
newArr[index][name] = e.target.value; //key and value
setData(newArr);
}
return (
<React.Fragment>
{data.map((datum, index) => {
<li key={datum.name}>
<input type="text" name="name" value={datum.name} onChange={updateFieldChanged(index)} />
</li>
})}
</React.Fragment>
)
const [datas, setDatas] = useState([ { id: 1, name: 'john', gender: 'm' } { id: 2, name: 'mary', gender: 'f' } ]);
const updateFieldChanged = (index, e) => { const updateData = { ...data[index], name: e.target.name }
setData([...data.slice(0, index), updateData, ...data.slice(index + 1)]) }
I am late to reply but I had also same problem, so I got solution through this query. Have a look on it if it can help you.The example that I did is that I have a state , named OrderList, and so a setter for it is SetOrderList. To update a specific record in a list, am using SetOrderList and passing it a map function with that list of which I need to change, so I will compare Index or Id of my list, where it will match, I will change that specific record.
const Action = (Id, Status) => { //`enter code here`Call this function onChange or onClick event
setorderList([...orderList.map((order) =>
order.id === Id ? { ...order, status: Status } : order
),
]);
}
complete example for update value based on index and generate input based on for loop....
import React, { useState,useEffect } from "react";
export default function App() {
const [datas, setDatas] =useState([])
useEffect(() => {
console.log("datas");
console.log(datas);
}, [datas]);
const onchangeInput = (val, index) =>{
setDatas(datas=>({
...datas,
[index]: val.target.value
}))
console.log(datas);
}
return (
<>
{
(() => {
const inputs = [];
for (let i = 0; i < 20; i++){
console.log(i);
inputs.push(<input key={i} onChange={(val)=>{onchangeInput(val,i)}} />);
}
return inputs;
})()
}
</>
);
}
const MyCount = () =>{
const myData = [
{
id: 1,
name: 'john',
gender: 'm'
},
{
id: 2,
name: 'mary',
gender: 'f'
}
]
const [count, setCount] = useState(0);
const [datas, setDatas] = useState(myData);
const clkBtn = () =>{
setCount((c) => c + 1);
}
return(
<div>
<button onClick={clkBtn}>+ {count}</button>
{datas.map((data, index) => (
<li key={index}>
<input
type="text"
value={data.name}
onChange={(e) => {
data.name = e.target.value;
setDatas([...datas]);
}}
/>
</li>
))}
</div>
)
}
Base on #Steffan, thus use as your way:
const [arr,arrSet] = useState(array_value);
...
let newArr = [...arr];
arr.map((data,index) => {
newArr[index].somename= new_value;
});
arrSet(newArr);
Use useEffect to check new arr value.
A little late to the party, but it is an option to spread the contents of the array in a new object, then replacing the desired object in the selected index and finally producing an array from that result, it is a short answer, probably not the best for large arrays.
// data = [{name: 'tom', age: 15, etc...}, {name: 'jerry', age: 10, etc...}]
// index = 1
setData(Object.values({...data, [index]: {...data[index], name: 'the mouse' }}))
// data = [{name: 'tom', age: 15, etc...}, {name: 'the mouse', age: 10, etc...}]
I have the below array of objects:
const [rows, setRows] = useState([
{id: 1, key: "key1", value: "value1"},
{id: 2, key: "key2", value: "value2"}
]);
And I have the below inputs as well:
<TextField name="key" onChange={(e)=> handleTable(e, record.id)} value{rows.filter...}/>
<TextField name="value" onChange={(e)=> handleTable(e, record.id)} value{rows.filter...}/>
Now I know that for handling the above inputs I should loop to find the appropriate object based on its ID then try to update it, I need one another for loop for the value of the above inputs as well, but that takes a long time in terms of hooks and reloading each time the user enters something, how can I handle the above situation, both updating and showing the appropriate item in the array?
Yes, you need to loop the textfields and pass the index to the change handler.
const [rows, setRows] = React.useState([
{ id: 1, key: "key1", value: "value1" },
{ id: 2, key: "key2", value: "value2" }
]);
const handleChange = (e,idx) => {
clone = [...rows];
let obj = clone[idx];
obj.value = e.target.value;
clone[idx] = obj;
setRows([...clone])
}
and Then you need to loop your rows with text field.
{ rows?.map((row, index) =>
<TextField value={rows[index]?.value} onChange={(e) =>
handleChange(e,index)} />
)}
This may help you to tweak your solution.
const [rows, setRows] = useState([
{ id: 1, key: "key1", value: "value1" },
{ id: 2, key: "key2", value: "value2" }
]);
const handleTable = (e, id) => {
const newRows = [...rows]; //spread the rows array into a new array
const index = rows.find((item, i) => {
if (item.id === id) return i;
}); //found the index using the id
if (e.target.name === "key") {
newRows[index].key = e.target.value; // update using the index
} else {
newRows[index].value = e.target.value;
}
setRows(() => [...newRows]);
};
<TextField name="key" onChange={(e)=> handleTable(e, record.id)} value{rows.filter...}/>
<TextField name="value" onChange={(e)=> handleTable(e, record.id)} value{rows.filter...}/>
if there is a better way plz edit
const [value , setValue]=useState([])
in html :
*need separate state for each input elements
<input value={value[key] ? value[key] : ""} onChange={(e) => handleSetValue(e.target.value, key)}/>
set value func() :
function handleSetValue(e, key) {
setValue(s => {
const newArr = s.slice();
newArr[key] = e;
return newArr;
});
}
I have a MenuOptions component that I pass an options prop to. Options is a large array of objects. Each object has a nested array called 'services' inside services is an object with a key 'serviceType' which is the only value I want. I want to take all those values, push them into a new array and remove any duplicates if there are any, and then map through that new array and display each item in an 'option' html tag.
here is my createArray function:
const createNewArray = () => {
let optionsArr = []
let uniqueArr;
options.map((option) => {
option.services.map((service) => optionsArr.push(service.serviceType))
})
uniqueArr = [...new Set(optionsArr)];
return uniqueArr;
}
uniqArr seems to be holding what I want but now I want to set this to a piece of global state. Trying something like this does not work. array seems to still be set as null
const [array, setArray] = useState(null)
useEffect(() => {
setArray(createNewArray())
}, [])
any solutions? Thanks
1) You should add your array state initial value as an empty array:
const [array, setArray] = useState([]);
Live Demo
2) You can simplify the creating of a new array as:
const createNewArray = () => [
...new Set(options.flatMap((o) => o.services.map((obj) => obj.serviceType)))
];
3) set array state in useEffect as:
useEffect(() => {
setArray(createNewArray());
}, []);
From your description is this your data?
const options = [{
services: [
{
serviceType: 'serviceType',
}
],
},{
services: [
{
serviceType: 'serviceType',
}
],
},{
services: [
{
serviceType: 'serviceType',
}
],
},
{
services: [
{
serviceType: 'serviceType',
}
],
}]
here is the solution
const uniq = (a) => [...new Set(a)];
const createNewArray = (array) => {
const c = [...array]
const newArray = []
for (let i = 0; i < c.length; i++) {
const e = c[i];
for (let ii = 0; ii < e.length; ii++) {
const ee = e[ii].serviceType;
newArray.push(ee);
}
}
const toReturn = uniq(newArray)
return toReturn;
}
If you want unique options, just pass the options in and set them to the state after you massage the data.
const { useEffect, useMemo, useState } = React;
const unique = (arr) => [...new Set(arr)];
const uniqueOptions = (options) =>
unique(options.flatMap(({ services }) =>
services.map(({ serviceType }) => serviceType)));
const data = {
options: [
{ services: [{ serviceType: "Home" } , { serviceType: "About" }] },
{ services: [{ serviceType: "About" } , { serviceType: "Help" }] },
{ services: [{ serviceType: "Help" } , { serviceType: "Home" }] },
],
};
const MenuOptions = (props) => {
const { options } = props;
const [opts, setOpts] = useState([]);
useEffect(() => setOpts(uniqueOptions(options)), [options]);
return useMemo(
() => (
<select>
{opts.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
),
[opts]
);
};
const App = ({ title }) =>
useMemo(
() => (
<div>
<h1>Services</h1>
<form>
<MenuOptions options={data.options} />
</form>
</div>
),
[]
);
ReactDOM.render(<App />, document.getElementById("react-app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react-app"></div>
I have an array with some dynamic value which will have array of array and according to inner array length need to show OR text.
Example:
JSON data:
const renderArr = [
{
label: "Abc",
value: []
},
{
label: "XyZ",
value: ["Test", "Exam"]
},
{
label: "Dex",
value: []
},
{
label: "Mno",
value: ["Momo", "Pizza"]
},
{
label: "Pqr",
value: []
}
];
According to above structure there are value in renderArr[1].value and renderArr[3].value. So i need to show "OR" text between these in UI
Expected Logic:
I want to show OR condition based on below condition
let currentValue = renderArr[index].value.length > 0 then
If nextValue = renderArr[index+1].value.length > 0 then show OR condition, NOTE: it'll check till it not found the value.length > 0 for every index
Below is my logic, which is not working properly.
My Logic:
shouldShowNextOr = (value, rootIndex) => {
let shouldShow = false;
let f = renderArr;
if (rootIndex < 4) {
switch (rootIndex) {
case 0:
shouldShow =
f[1].value.length > 0 ||
f[2].value.length > 0 ||
f[3].value.length > 0 ||
f[4].value.length > 0
? true
: false;
break;
case 1:
shouldShow =
f[2].value.length > 0 ||
f[3].value.length > 0 ||
f[4].value.length > 0
? true
: false;
break;
case 2:
shouldShow =
f[3].value.length > 0 || f[4].value.length > 0 ? true : false;
break;
case 3:
shouldShow = f[4].value.length > 0 ? true : false;
break;
default:
shouldShow = false;
}
}
return shouldShow;
};
Here is online codesandbox
Current Output
Expected Output
Filter the renderArray (data in my example), and remove all items with empty value array. Render the filtered array, and if an item is not the last (i < filteredData.length - 1) you can render "OR" after it:
const { useMemo } = React;
const App = ({ data }) => {
const filteredData = useMemo(() => data.filter(o => o.value.length), [data]);
return (
<div className="App">
{filteredData.map((o, i) => (
<React.Fragment key={o.label}>
<div>
<span>{o.label}</span> ={" "}
{o.value.join(" and ")}
</div>
{i < filteredData.length - 1 &&
<span>OR<br /></span>
}
</React.Fragment>
))}
</div>
);
};
const renderArr = [{"label":"Abc","value":[]},{"label":"XyZ","value":["Test","Exam"]},{"label":"Dex","value":[]},{"label":"Mno","value":["Momo","Pizza"]},{"label":"Pqr","value":[]}];
ReactDOM.render(
<App data={renderArr} />,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
If you need the original index, use Array.map() to include it in the object before filtering:
const { useMemo } = React;
const App = ({ data }) => {
const filteredData = useMemo(() =>
data
.map((o, index) => ({ ...o, index }))
.filter(o => o.value.length)
, [data]);
return (
<div className="App">
{filteredData.map((o, i) => (
<React.Fragment key={o.label}>
<div>
<span>{o.label} - {o.index}</span> ={" "}
{o.value.join(" and ")}
</div>
{i < filteredData.length - 1 &&
<span>OR<br /></span>
}
</React.Fragment>
))}
</div>
);
};
const renderArr = [{"label":"Abc","value":[]},{"label":"XyZ","value":["Test","Exam"]},{"label":"Dex","value":[]},{"label":"Mno","value":["Momo","Pizza"]},{"label":"Pqr","value":[]}];
ReactDOM.render(
<App data={renderArr} />,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Another way would be : Store all the index values of the renderArray which has value in another array(arr in my example). Check if rootIndex is part of arr.
Here's the code
arrIndex = [];
shouldShowNextOr = (value, rootIndex) => {
let shouldShow = false;
let f = renderArr;
let arr = this.arrIndex;
//storing all the indices which has value
if (arr.length === 0) {
f.forEach(function (a, index) {
if (a.value.length > 0) arr.push(index);
});
}
//check if root index exists
if (arr.includes(rootIndex) && arr.length > 1) {
//skip the last element in the array.
if (rootIndex === arr[arr.length - 1]) {
shouldShow = false;
} else {
shouldShow = true;
}
}
return shouldShow;
};
Input:
const renderArr = [
{
label: "Abc",
value: []
},
{
label: "XyZ",
value: ["Yes", "No"]
},
{
label: "Dex",
value: []
},
{
label: "Mno",
value: ["Hi","Hello"]
},
{
label: "Pqr",
value: ["Yes", "No"]
}
];
Output:
XyZ = Yes and No
OR
Mno = Hi and Hello
OR
Pqr = Yes and No
I have retrieved data stored using useState in an array of object, the data was then outputted into form fields. And now I want to be able to update the fields (state) as I type.
I have seen examples on people updating the state for property in array, but never for state in an array of object, so I don't know how to do it. I've got the index of the object passed to the callback function but I didn't know how to update the state using it.
// sample data structure
const data = [
{
id: 1,
name: 'john',
gender: 'm'
}
{
id: 2,
name: 'mary',
gender: 'f'
}
]
const [data, setData] = useState([]);
const updateFieldChanged = index => e => {
console.log('index: ' + index);
console.log('property name: '+ e.target.name);
setData() // ??
}
return (
<React.Fragment>
{data.map((data, index) => {
<li key={data.name}>
<input type="text" name="name" value={data.name} onChange={updateFieldChanged(index)} />
</li>
})}
</React.Fragment>
)
Here is how you do it:
// sample data structure
/* const data = [
{
id: 1,
name: 'john',
gender: 'm'
}
{
id: 2,
name: 'mary',
gender: 'f'
}
] */ // make sure to set the default value in the useState call (I already fixed it)
const [data, setData] = useState([
{
id: 1,
name: 'john',
gender: 'm'
}
{
id: 2,
name: 'mary',
gender: 'f'
}
]);
const updateFieldChanged = index => e => {
console.log('index: ' + index);
console.log('property name: '+ e.target.name);
let newArr = [...data]; // copying the old datas array
// a deep copy is not needed as we are overriding the whole object below, and not setting a property of it. this does not mutate the state.
newArr[index] = e.target.value; // replace e.target.value with whatever you want to change it to
setData(newArr);
}
return (
<React.Fragment>
{data.map((datum, index) => {
<li key={datum.name}>
<input type="text" name="name" value={datum.name} onChange={updateFieldChanged(index)} />
</li>
})}
</React.Fragment>
)
The accepted answer leads the developer into significant risk that they will mutate the source sequence, as witnessed in comments:
let newArr = [...data];
// oops! newArr[index] is in both newArr and data
// this might cause nasty bugs in React.
newArr[index][propertyName] = e.target.value;
This will mean that, in some cases, React does not pick up and render the changes.
The idiomatic way of doing this is by mapping your old array into a new one, swapping what you want to change for an updated item along the way.
setData(
data.map(item =>
item.id === index
? {...item, someProp : "changed", someOtherProp: 42}
: item
))
setDatas(datas=>({
...datas,
[index]: e.target.value
}))
with index being the target position and e.target.value the new value
You don't even need to be using the index ( except for the key if you want ) nor copying the old datas array,and can even do it inline or just pass data as an argument if you prefer updateFieldChanged to not be inline. It's done very quickly that way :
const initial_data = [
{
id: 1,
name: "john",
gender: "m",
},
{
id: 2,
name: "mary",
gender: "f",
},
];
const [datas, setDatas] = useState(initial_data);
return (
<div>
{datas.map((data, index) => (
<li key={index}>
<input
type="text"
value={data.name}
onChange={(e) => {
data.name = e.target.value;
setDatas([...datas]);
}}
/>
</li>
))}
</div>
);
};
This is what I do:
const [datas, setDatas] = useState([
{
id: 1,
name: "john",
gender: "m",
},
{
id: 2,
name: "mary",
gender: "f",
},
]);
const updateFieldChanged = (name, index) => (event) => {
let newArr = datas.map((item, i) => {
if (index == i) {
return { ...item, [name]: event.target.value };
} else {
return item;
}
});
setDatas(newArr);
};
return (
<React.Fragment>
{datas.map((data, index) => {
<li key={data.name}>
<input
type="text"
name="name"
value={data.name}
onChange={updateFieldChanged("name", index)}
/>
</li>;
<li key={data.gender}>
<input
type="text"
name="gender"
value={data.gender}
onChange={updateFieldChanged("gender", index)}
/>
</li>;
})}
</React.Fragment>
);
Spread the array before that. As you cannot update the hook directly without using the method returned by useState
const newState = [...originalState]
newState[index] = newValue
setOriginalState(newState)
This will modify the value of the state and update the useState hook if its an array of string.
const updateFieldChanged = index => e => {
name=e.target.name //key
let newArr = [...data]; // copying the old datas array
newArr[index][name] = e.target.value; //key and value
setData(newArr);
}
return (
<React.Fragment>
{data.map((datum, index) => {
<li key={datum.name}>
<input type="text" name="name" value={datum.name} onChange={updateFieldChanged(index)} />
</li>
})}
</React.Fragment>
)
const [datas, setDatas] = useState([ { id: 1, name: 'john', gender: 'm' } { id: 2, name: 'mary', gender: 'f' } ]);
const updateFieldChanged = (index, e) => { const updateData = { ...data[index], name: e.target.name }
setData([...data.slice(0, index), updateData, ...data.slice(index + 1)]) }
I am late to reply but I had also same problem, so I got solution through this query. Have a look on it if it can help you.The example that I did is that I have a state , named OrderList, and so a setter for it is SetOrderList. To update a specific record in a list, am using SetOrderList and passing it a map function with that list of which I need to change, so I will compare Index or Id of my list, where it will match, I will change that specific record.
const Action = (Id, Status) => { //`enter code here`Call this function onChange or onClick event
setorderList([...orderList.map((order) =>
order.id === Id ? { ...order, status: Status } : order
),
]);
}
complete example for update value based on index and generate input based on for loop....
import React, { useState,useEffect } from "react";
export default function App() {
const [datas, setDatas] =useState([])
useEffect(() => {
console.log("datas");
console.log(datas);
}, [datas]);
const onchangeInput = (val, index) =>{
setDatas(datas=>({
...datas,
[index]: val.target.value
}))
console.log(datas);
}
return (
<>
{
(() => {
const inputs = [];
for (let i = 0; i < 20; i++){
console.log(i);
inputs.push(<input key={i} onChange={(val)=>{onchangeInput(val,i)}} />);
}
return inputs;
})()
}
</>
);
}
const MyCount = () =>{
const myData = [
{
id: 1,
name: 'john',
gender: 'm'
},
{
id: 2,
name: 'mary',
gender: 'f'
}
]
const [count, setCount] = useState(0);
const [datas, setDatas] = useState(myData);
const clkBtn = () =>{
setCount((c) => c + 1);
}
return(
<div>
<button onClick={clkBtn}>+ {count}</button>
{datas.map((data, index) => (
<li key={index}>
<input
type="text"
value={data.name}
onChange={(e) => {
data.name = e.target.value;
setDatas([...datas]);
}}
/>
</li>
))}
</div>
)
}
Base on #Steffan, thus use as your way:
const [arr,arrSet] = useState(array_value);
...
let newArr = [...arr];
arr.map((data,index) => {
newArr[index].somename= new_value;
});
arrSet(newArr);
Use useEffect to check new arr value.
A little late to the party, but it is an option to spread the contents of the array in a new object, then replacing the desired object in the selected index and finally producing an array from that result, it is a short answer, probably not the best for large arrays.
// data = [{name: 'tom', age: 15, etc...}, {name: 'jerry', age: 10, etc...}]
// index = 1
setData(Object.values({...data, [index]: {...data[index], name: 'the mouse' }}))
// data = [{name: 'tom', age: 15, etc...}, {name: 'the mouse', age: 10, etc...}]