Update state value of single object in an array - javascript

I have list of items where I want to display a loader and hide it upon completing certain action.
For example, here is my array items
[
{
"id": "69f8f183-b057-4db5-8c87-3020168307c5",
"loading": null
},
{
"id": "30d29489-0ba9-4e00-bc28-8ad34ff1a285",
"loading": true
},
{
"id": "5f54ebbd-d380-4a54-bb1d-fc6c76dd1b72",
"loading": false
}
]
I am adding item to array with loading value as null the reason is. I want to process as soon as the the state is updated, hence I am using useEffect hook to observe for any change, if any new item with loading value null is added, then I proceed for action.
My problem is, when I try to modify a single loading value to false, it gives me weird behaviour and set all loading value to false.
What I want to have it, when I change the loading value of a single item in array, then the UI should re-render only for the changed item.
If you want to have a look at fiddle with working example, here is the link https://codesandbox.io/s/d8lh4-d8lh4
Where am I going wrong here?

It's simple use this code:
setTimeout(() => {
setItems((existingItems) =>
existingItems.map((item) =>
item.id === newItem?.id ? { ...item, loading: false } : item
)
);
}, 2000);

Looking at your code, I think the issue is related to accessing the wrong value of newItem and items in setTimeout. Both of them can be solved by doing something similar to the one below.
const handleUpload = newItem => {
// set loading to false to new item after 2 seconds
setTimeout(
theNewItem => {
setItems(exisitingItems =>
exisitingItems.map(item =>
item.id === theNewItem.id ? { ...item, loading: false } : theNewItem,
),
);
},
2000,
newItem,
);
};

You have [items] dependency in your useEffect, which is calling setItems in loop in your handleUpload function.

Related

Vue select all checkbox bad performance

I have a list objects displayed, each having a checkbox to select it. I also have a checkbox at the top to select the checkbox for every object, kinda like this (assume [ ] is a checkbox):
[ ] Select all
[ ] Object 1
[ ] Object 2
[ ] Object 3
The problem I have is when I have about 100 objects and clicking "Select all", the web page freezes for a good few seconds. There is also a search bar to filter the object, but I tested this by removing it and the performance is just as slow. Each object has a property selected so we know which object is selected. Below are some snippets of my code:
HTML:
<checkbox-input
id="documentSelectAll"
:model-value="operatingRows.length === 0 ? false : allSelected"
#update:modelValue="allSelectPressed" // Calls vuex function below
/>
---
<tr
v-for="(row, index) in operatingRows"
:key="index"
>
<document-table-row
:row-idx="index"
:row-data="row"
:fields="row.fields"
:hidden-column-indices="hiddenColumnIndices"
#toggleSelectedOnRow="toggleSelectedOnRow(row.id)" // Calls vuex to select individual row
/>
</tr>
Computed properties:
operatingRows() {
const showRow = (r) => {
// Some search code, irrelevant here
};
return this.sorted.filter(showRow); // 'sorted' comes from vuex
},
selectedRows() {
return this.operatingRows.filter((r) => r.selected);
},
numSelected() {
return this.selectedRows.reduce((prev, cur) => (cur.selected ? prev + 1 : prev), 0);
},
allSelected() {
return this.numSelected === this.operatingRows.length;
},
Vuex store:
getters: {
...storeStateGetters,
sorted: (state) => state.sorted,
},
---
mutations: {
...storeStateMutations,
SET_ALL_SELECTED_ON_SORTED(state, isSelected) {
state.sorted.map((r) => {
const rn = r;
rn.selected = isSelected;
return rn;
});
},
},
I think it might be to do with the fact that there are too many computed properties? I tried removing them individually (and the associated code) but the performance still seems bad, thus I am not able to pin point the issue to any particular piece of code, rather I think it's to do with the architecture as a whole.
Any help appreciated.
Turns out it was because of the mutation. To fix this, the mutation code was moved to an action which calls a mutation to set the state of the sorted array.
selectOnSorted: ({ commit, rootGetters }, isSelected) => {
const selectedSorted = rootGetters['documents/sorted'].map((doc) => ({
...doc,
selected: isSelected,
}));
commit('SET_SORTED', selectedSorted);
},

React JSX Dynamically changing the state of a component. Best practice and why

I am pretty new at React and am following a video on youtube and working along + adding some more on top of it.
I have a button which when clicked, it decreases the value of object. In the video, the teacher copies the state to a new array, finds the index of the item, does the manipulation needed, and then sets the state again using this.setState()
This is the hardcoded state which I am using to practice. I want that when a button is clicked, the value to be reduced by 1 unless the value is smaller than or equal to 0.
state = {
counters: [
{ id: 1, name: "Drink 1", value: 4 },
{ id: 2, name: "Drink 2", value: 0 },
{ id: 3, name: "Drink 3", value: 0 },
{ id: 4, name: "Drink 4", value: 0 },
],
};
//and what I'm passing down to other components:
{this.state.counters.map((counter) => (
<Counter
key={counter.id}
counter={counter}
onDeletion={this.handleDeletion}
onIncrement={this.handleIncrement}
onDecrement={this.handleDecrement}
></Counter>
))}
Code from video:
handleDecrement = (counter) => {
const counters = [...this.state.counters];
const indexOfCounters = counters.indexOf(counter);
counters[indexOfCounters] = { ...counter };
counters[indexOfCounters].value <= 0
? (counters[indexOfCounters].value = 0)
: counters[indexOfCounters].value--;
this.setState({ counters });
};
Here is the code which I came up with that gives the button the same functionality:
handleDecrement = (counter) => {
counter.value <= 0 ? (counter.value = 0) : counter.value--;
this.setState(counter);
};
Both ways provide the functionality needed, I am just hesitant to go my way in case this goes against best practice.
From reading the docs and similar state related questions, I can guess that the way provided in the video changes the complete state and my way only changes an object within it. Is the youtube code the correct way to approach this because the whole state is being set and we keep a single source of truth? Is my way bad practice?
Video is by Programming with Mosh btw: https://www.youtube.com/watch?v=Ke90Tje7VS0
I think you're just confused on the reason behind updating the counters array instead of an object(counter) inside it.
It is because the state should always be updated in an immutable way. In your case, since the value is in an object that is in turn in an array, the counters array and the counter object should have a new reference after updating to properly inform React that the state has changed.
In your snippet this.setState(counter); will overwrite the state with the counter(so the other counters are removed) and you're also mutating the object.
If you want to make the code a bit concise while also making sure that the state is updated immutably, here's an alternative:
handleDecrement = (counter) => {
this.setState(prevState => ({counters: prevState.counters.map(c => {
return (c.id === counter.id && c.value > 0) ? {...c, value: c.value - 1} : c;
})}));
};
In the above snippet map creates a new array and the {} object literal syntax creates a new counter object.

ReactJS: Updating array inside object state doesn't trigger re-render

I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.
It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}
Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}

Updating parent component only after multiple child components have completed running

I have a Parent react component with multiple child components that are created through a .map() function. I am passing in a function addCallback() as child props so I have a reference and can trigger all child's handleRun() function via the Parent.
I'm trying to update state of my Parent component to running = true when all children are running and to running = false and render said status on the parent when all children have completed running. However the state doesn't seem to update in the particular sequence I specify.
Here is how I'm doing it:
let promise1 = this.setState({isRunning: true},
() => {
this.state.childRef.map(x => x())
});
Promise.all([promise1])
.then(() => this.setState({isRunning: false}))
Here's the entire code in codesandbox: link
Would appreciate your help as I'm still pretty new to React (and Javascript in general). Thanks!
Cause runSomething is not a Promise. You must change.
runSomething() {
return new Promise((resolve, reject) => {
this.setState({ status: "running" });
// simulate running something that takes 8s
setTimeout(() => {
this.setState({ status: "idle" });
resolve(true);
}, 3000);
});
}
A working sandbox here https://codesandbox.io/s/fragrant-cloud-5o2um
Using async in a function declaration automatically returns a Promise wrapped around whatever you are returning from your function. In your case, it's undefined. This is why your current code is not throwing any errors at the moment.
You will need a mechanism to wait for the setTimeout. Changing the runSomething function like this will work
async runSomething() {
this.setState({ status: "running" });
// simulate running something that takes 8s
return new Promise(resolve => {
setTimeout(() => {
this.setState({ status: "idle" }, resolve);
}, 3000);
});
}
Do notice the line this.setState({ status: "idle" }, resolve);. It makes sure that your promise resolves not only after the setTimeout but also after the child's state is changed to "idle". Which is the correct indication that your child component has moved to "idle" state.
Codesandbox: https://codesandbox.io/s/epic-boyd-12hkj
Here is the sandbox implementation of what you are trying to achieve. Sanbox
Here i have created a state in parent component that will be updated when child is running.
this.state = {
callbacks: [],
components: [
{
index: 0, // we don't need this field its just for your info you can just create [true,false] array and index will represent component index.
status: false
},
{
index: 1,
status: false
}
]
};
When all the status in component array is true we update the idle status of parent to running.
getAllRunningStatus() {
let { components } = this.state;
let checkAllRunning = components.map(element => element.status);
if (checkAllRunning.indexOf(false) === -1) { // you can also use !includes(false)
return true;
}
return false;
}
inside your render function
<h1>Parent {this.getAllRunningStatus() ? "running" : "idle"}</h1>
Note:- I have just written a rough code. You can optimise it as per your requirements. Thanks

React setState of boolean value not updating

New to React, trying to update the state of an object where on property already has a set boolean value. However, it seems like the state is not updating.
I understand that state is update asynchronously, maybe that could coming into play here? I don't believe I can use the setState method that takes an object and callback function because I need access the the previous state.
Here is my initial state:
items: [
{
id: 0,
title: 'Dev Grub',
selected: false
},
...
]
And here is my event handler:
handleCardClick(id, card) {
this.setState((preState, props) => ({
[preState.items[id].selected]: [preState.items[id].selected] ? false : true
}));
console.log('new state: ', this.state.items[id].selected);
}
I've also tried this instead of the ternary: ![card.selected]
updating just a property at the second level of the state won't work. use something like below:
handleCardClick(id, card) {
let items = [...state.items];
items[id].selected = items[id].selected ? false : true
this.setState(() => ({
items
}));
}
React setState doesn't work this way, it doesn't update state right away but rather enqueues the change to update it at some point in the future.
If you want to do something as soon as the state has been updated you can use callback parameter
this.setState((preState, props) => ({
[preState.items[id].selected]: [preState.items[id].selected] ? false : true
}), () => console.log('new state: ', this.state.items[id].selected);)
See docs on setState
setState is async, you console log after the state has been changed like ths
handleCardClick = (id, card) => {
this.setState(
{
[this.state.items[id].selected]: [this.state.items[id].selected]
? false
: true,
},
() => console.log('new state: ', this.state.items[id].selected),
);
};

Categories

Resources