useCallback not updating array with object - reactjs

I am trying to update array with objects using useCallback but instead of updating object it is adding or appending one too.
const items = [
{
id: 101,
name: 'Mae Jemison',
},
{
id: 201,
name: 'Ellen Ochoa',
},
];
const [headings, setHeadings] = useState(items);
const handleHeadingTextChange = useCallback(
(value, id) => {
let items2 = headings;
items2 = items2.map((item, key) => {
if (items2[key].id == id) {
items2[key].name = value
}
return items2
});
setHeadings((prevState) => [...prevState, items2]) // adding, not updating
//setHeadings((prevState) => [...prevState, ...items2]) // try 2 : still adding
},
[setHeadings],
);
<input type="text" id="101" value="" onChange={handleHeadingTextChange} />
So, on change expected output is
items = [
{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
},
];
But Instead I am getting
items = [
{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
},
[{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
}]
];
I am not sure how to set value in setHeading function so that it only update the value and not appending one too. Is there way to make it update only?

problems
.map iterates over the entire array. that doesn't make sense if you are trying to just update a single item.
onChange takes an event handler, but your handler accepts (value, id)
the value property of your <input> should be set from the object's name property
solution
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = key => event => {
setHeadings(prevState => {
return [
// elements before the key to update
...prevState.slice(0, key),
// the element to update
{ ...prevState[key], name: event.currentTarget.value },
// elements after the key
...prevState.slice(key + 1),
]
})
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
)
}
improvement
Imagine having to write that function each time you have a component with array or object state. Extract it to a generic function and use it where necessary -
// generic functions
function arrUpdate(arr, key, func) {
return [...arr.slice(0, key), func(arr[key]), ...arr.slice(key + 1)]
}
function objUpdate(obj, key, func) {
return {...obj, [key]: func(obj[key])}
}
// simplified component
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = key => event => {
setHeadings(prevState =>
// update array at key
updateArr(prevState, key, elem =>
// update elem's name property
updateObj(elem, "name", prevName =>
// new element name
event.currentTarget.value
)
)
)
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
)
}
multiple arrow functions?
Curried functions make writing your event handlers easier, but maybe you've never used them before. Here's what the uncurried version would look like -
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = (key, event) => {
setHeadings(prevState =>
// update array at key
updateArr(prevState, key, elem =>
// update elem name property
updateObj(elem, "name", prevName =>
// new element name
event.currentTarget.value
)
)
)
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={e => updateName(key, e)} />
)
}

Related

How to update object in array of objects

I have following part of React code:
This is handler for adding new object item to the array.
So I am checking if object exist in whole array of objects. If yes I want to increase quantity of this object by 1. Else I want to add object and set quantity to 1.
I am getting error when I want to add 2nd same product (same id)
Uncaught TypeError: Cannot assign to read only property 'quantity' of object '#<Object>'
export const Component = ({ book }: { book: Book }) => {
const [basket, setBasket] = useRecoilState(Basket);
const handleAddBookToBasket = () => {
const find = basket.findIndex((product: any) => product.id === book.id)
if (find !== -1) {
setBasket((basket: any) => [...basket, basket[find].quantity = basket[find].quantity + 1])
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }])
}
}
EDIT:
if (find !== -1) {
setBasket((basket: any) =>
basket.map((product: any) => ({
...product,
quantity: product.quantity + 1,
}))
);
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }]);
}
data structures
I'd say the root of your "problem" is that you chose the wrong data structure for your basket. Using Array<Item> means each time you need to update a specific item, you have to perform O(n) linear search.
If you change the basket to { [ItemId]: Item } you can perform instant O(1) updates. See this complete example below. Click on some products to add them to your basket. Check the updated basket quantity in the output.
function App({ products = [] }) {
const [basket, setBasket] = React.useState({})
function addToBasket(product) {
return event => setBasket({
...basket,
[product.id]: {
...product,
quantity: basket[product.id] == null
? 1
: basket[product.id].quantity + 1
}
})
}
return <div>
{products.map((p, key) =>
<button key={key} onClick={addToBasket(p)} children={p.name} />
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
const products = [
{ id: 1, name: "ginger" },
{ id: 2, name: "garlic" },
{ id: 3, name: "turmeric" }
]
ReactDOM.render(<App products={products}/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
object update
As a good practice, you can make a function for immutable object updates.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
export { update }
Then import it where this behavior is needed. Notice it's possible to use on nested object updates as well.
// App.js
import * as Obj from "./Obj"
function App({ products = [] }) {
// ...
function addToBasket(product) {
return event => setBasket(
Obj.update(basket, product.id, (p = product) => // ✅
Obj.update(p, "quantity", (n = 0) => n + 1) // ✅
)
)
}
// ...
}
object remove
You can use a similar technique to remove an item from the basket. Instead of coupling the behavior directly in the component that needs removal, add it to the Obj module.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
const remove: (o, key) => { // ✅
const r = { ...o }
delete r[key]
return r
}
export { update, remove } // ✅
Now you can import the remove behaviour in any component that needs it.
function App() {
const [basket, setBasket] = React.useState({
1: { id: 1, name: "ginger", quantity: 5 },
2: { id: 2, name: "garlic", quantity: 6 },
3: { id: 3, name: "tumeric", quantity: 7 },
})
function removeFromBasket(product) {
return event => {
setBasket(
Obj.remove(basket, product.id) // ✅
)
}
}
return <div>
{Object.values(basket).map((p, key) =>
<div key={key}>
{p.name} ({p.quantity})
<button onClick={removeFromBasket(p)} children="❌" />
</div>
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
// inline module for demo
const Obj = {
remove: (o, key) => {
const r = {...o}
delete r[key]
return r
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In your setBasket your should create a new object instead of updating current one
your code should look like these one :
{...basket, ...{
quantity : basket.quantity + 1
}}

prevent api from being called before state is updated

I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);
So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])

Why the table data is not updated immediately after performing an action for the table

I have a table that has an action to delete..like this:
const deleteRow = (row) => {
let indexOfDeleted = -1;
let data = tableData;
data.forEach((item, index) => {
if (item.instrumentId === row.instrumentId) {
indexOfDeleted = index;
}
})
data.splice(indexOfDeleted, 1);
setTableData(data)
};
The data is deleted but I have to refresh it so that it is not displayed in the table.It does not seem to be rerender. What should I do?
for table:
const schema = {
columns: [
{
field: "persianCode",
title: "title",
},
],
operations: [
{
title: "delete",
icon: (
<DeleteIcon
className={clsx(classes.operationsIcon, classes.deleteIcon)}
/>
),
action: (row) => deleteRow(row),
tooltipColor: theme.palette.color.red,
}
],
};
You are mutating the state variable, in your deleteRow function. You should update the state with a copied array:
const deleteRow = (row) => {
setTableData(table => table.filter(data => data.instrumentId !== row.instrumentId))
};
Instead of finding the index and splicing it, you can just use the filter function. Since it returns a new array, we don't worry about mutating the state variable!
you will have to use Spread operator to reflect changes in react dom..
const deleteRow = (row) => {
let indexOfDeleted = -1;
let data = tableData;
data.forEach((item, index) => {
if (item.instrumentId === row.instrumentId) {
indexOfDeleted = index;
}
})
data.splice(indexOfDeleted, 1);
setTableData([...data]) /// like this
};

How to setState for react useState hook, array of objects?

Here, using an array I have mapped the input boxes, and the value which has entered in that array should be stored in an usestate hook and on click of submit button it should console the array of objects. How to acheive this? Thank You.
const AddCheckPoint = ({ master }) => {
const [addChecklist, setAddChecklist] = useState([])
const [addChecklistValues, setAddChecklistValues] = useState({})
const submit = () => {
setShow(false)
console.log(JSON.stringify(addChecklist))
}
const getFormDetails = (val, id) => {
setAddChecklistValues(prevState => ({
...prevState,
ch_id: id,
value: val
}));
setAddChecklist(prevArray => [...prevArray, addChecklistValues])
}
return (
<>
<Row><Col><b>SLno</b></Col><Col><b>Checkpoint</b></Col><Col><b>Value</b></Col></Row>
{master && master.map((r, i) => {
return (<Form.Group key={i} className="mb-3" controlId={i}>
<Row><Col><Form.Label>{r.slno}</Form.Label></Col><Col><Form.Label>{r.checkPoint}</Form.Label></Col><Col><Form.Control type="text" placeholder="Enter Username" onChange={e => getFormDetails(e.target.value, r._id)} /></Col></Row>
</Form.Group>)
})}
<Button variant="primary" onClick={submit}>Submit</Button>
</>
);
}
export default AddCheckPoint
And the prop "master" looks like the follows
[
{
"_id": "61028558b45073399077becd",
"slno": "A1",
"checkPoint": "Position of adaptor CAT",
"cellName": "PC - 1",
"createdAt": "2021-07-29T10:39:20.902Z",
"updatedAt": "2021-07-29T10:39:20.902Z",
"__v": 0
},
{
"_id": "61028567b45073399077becf",
"slno": "A2",
"checkPoint": "Flush height of Adaptor CAT position",
"cellName": "PC - 1",
"createdAt": "2021-07-29T10:39:35.752Z",
"updatedAt": "2021-07-29T10:39:35.752Z",
"__v": 0
}
]
Just add simple by create a new value and pass it into setAddChecklistValues and setAddChecklist
const getFormDetails = (val, id) => {
const newValue = {
ch_id: id,
value: val,
};
setAddChecklistValues(newValue);
setAddChecklist((prevArray) => [...prevArray, newValue]);
};
Here as you are using onchange u must use debouncing technique to get correct the array of the objects. Refer Following url:
https://www.telerik.com/blogs/debouncing-and-throttling-in-javascript or you can replace below function in your code :
const getFormDetails = (val, id) => {
setAddChecklistValues((prevState) => ({
...prevState,
ch_id: id,
value: val
}));
setAddChecklist((prevArray) => {
const k = prevArray.filter((e) => {
return e.ch_id === addChecklistValues.ch_id;
});
if (k.length > 0) {
prevArray.map((e) => {
if (e.ch_id === addChecklistValues.ch_id) {
e.value = addChecklistValues.value;
}
return e;
});
return prevArray;
}
return [...prevArray, addChecklistValues];
});
};

Merge 2 arrays into 1 array and filter this merged array,

So I have 2 arrays of objects: planned, backlog.
const planned = [
{
order_number: "1",
part_name: "example",
part_number: "1",
process_id: 1
},
....
];
const backlog = [
{
order_number: "2",
part_name: "example",
part_number: "2",
process_id: 2
},
....
];
I am trying to filter both of them at the same time, each one individually that's not a problem.
So what I am actually doing is first adding key of planned to planned array and backlog to backlog array in order to know later from where the order originally came from.
var newPlanned = planned.map(function (el) {
var o = Object.assign({}, el);
o.planned = true;
return o;
});
var newBacklog = backlog.map(function (el) {
var o = Object.assign({}, el);
o.backlog = true;
return o;
});
Later I am merging the 2 arrays into 1 array, and filtering the merged array with one input onChange, but what I can't achieve is to render the "planned" and "backlog" arrays separately from the new merged array.
const newPlannedAndBacklog = [...newBacklog, ...newPlanned];
Link to codeSandBox:
SandBox
I added a type instead of a boolean property for several types. This may be more scalable so you can add more types (refined, done, etc.), but let me know if you'd like the boolean property.
const combinedList = [
...planned.map((item) => ({ ...item, type: 'planned' })),
...backlog.map((item) => ({ ...item, type: 'backlog' })),
];
const getFilteredList = (type) => combinedList.filter((item) => item.type === type);
UPDATE
I believe this is the behavior you want. Let me know if not and I'll update the answer.
// Combines all backlog and planned stories into one array
const newPlannedAndBacklog = [
...planned.map((item) => ({ ...item, type: 'planned' })),
...backlog.map((item) => ({ ...item, type: 'backlog' })),
];
// Filters property values based on input value
const getFilteredList = (filter) => newPlannedAndBacklog
.filter(item => Object
.values(item)
.map(String)
.some(v => v.includes(filter))
);
export default function App() {
const [form, setForm] = useState({});
const [stories, setStories] = useState([]);
const handleChange = (event) => {
const { name, value } = event.target;
setForm({ [name]: value });
setStories(getFilteredList(value));
};
// Separates the return of getfilteredList based on type
const renderDiv = (type) => stories
.filter((story) => story.type === type)
.map((story) => {
// Render each property individually:
return Object.entries(story).map(([key, value]) => {
const content = <>{key}: {value}</>;
if (key === 'order_number') return <h3>{content}</h3>;
return <p>{content}</p>;
});
});
return (
<div className="App">
<h2>PlannedAndBacklog</h2>
<p>Search for planned and backlog:</p>
<input
name='type'
onChange={handleChange}
value={form.type || ''}
/>
<div>{renderDiv('backlog')}</div>
<div>{renderDiv('planned')}</div>
</div>
);
};

Resources