Set state of nested array - javascript

I have this class:
class Board {
this.state = {
lists : [{
id: 0,
title: 'To Do',
cards : [{id : 0}]
}]
}
And want to use setState on the 'cards' array inside of the 'lists' state array. Previously, I had the cards array in a child component but I have now moved it up to the Board class. This is the function that I had before.
deleteCards(id){
this.setState({
cards: this.state.cards.filter(card => card.id !== id)
});
}
How can I change it so that it works now that cards is inside another array?
I was unable to solve it looking at these posts:
ReactJS - setState of Object key in Array
How to edit an item in a state array?

To do it all within setState (note that the first argument to setState is an updater function where its first argument is a reference to the previous state):
If you can provide the listId from the caller:
deleteCards(listId, cardId) {
this.setState(prevState => ({
lists: prevState.lists.map((list) => {
if (list.id !== listId) {
return list
}
return {
...list,
cards: list.cards.filter(card => card.id !== cardId)
}
})
}))
}
If you can not provide the listId from the caller:
deleteCards(id) {
this.setState(prevState => ({
lists: prevState.lists.map((list) => {
if (list.cards.some(card => card.id === id)) {
return {
...list,
cards: list.cards.filter(card => card.id !== id)
}
}
return list
})
}))
}

You should attempt to use the new rest and spread syntax...
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
const newListItem = {this.state.lists[0].cards.filter....}
this.setState({lists: [...this.state.lists.cards, newListItem]})
I would have made this a comment but it would be pretty hard to read. This is just an example you need to actually write a filter.

Related

Having problem with IncreaseItem function

My result is seeing item.acf.count as string and not as number. How can I convert this to number?
Here is the function below.
increaseItem = (id) => {
const { cart } = this.state;
cart.forEach(item => {
if (item.id === id) {
item.acf.count += 1
}
})
this.setState({
cart:cart
})
}
My result is seeing item.acf.count as string and not as number. please
how can I convert is to number
You should ensure that the initial item.acf.count state is a number type so this count: item.acf.count + 1 operation works correctly and returns a number type. So long as your state updaters maintain the state invariant of item.acf.count being a number it should work as expected.
Additionally, the increaseItem handler is mutating the cart state and not creating new array/object references.
increaseItem = (id) => {
const { cart } = this.state; // <-- cart is reference to cart state
cart.forEach(item => {
if (item.id === id) {
item.acf.count += 1; // <-- mutation!
}
});
this.setState({
cart: cart // <-- cart state reference back into state
})
}
You should instead shallow copy the cart and then also shallow copy the cart item (any any other nested properties) you want to update. I also suggest using a functional state update so you are correctly updating from the previous state and not any state value closed over in increaseItem callback scope.
increaseItem = (id) => {
this.setState(prevState => ({
cart: prevState.cart.map(item => item.id === id
? {
...item,
acf: {
...item.acf,
count: item.acf.count + 1
}
}
: item),
}));
}

React setState not updating checkbox checked state

I'm following a React beginners tutorial making a todo app as an example.
in the App.js, there is a handleChange method that will update the state whether the checkbox is checked or not, and passes it into the TodoItem component
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
console.log(id)
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItems = this.state.todos.map(item => <TodoItem key={item.id} item={item} handleChange={this.handleChange}/>)
return (
<div className="todo-list">
{todoItems}
</div>
)
}
}
export default App
TodoItem.js
function TodoItem(props) {
return (
<div className="todo-item">
<input
type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<p>{props.item.text}</p>
</div>
)
}
export default TodoItem
it successfully displays the list and the console log correctly displays the checkbox clicked, however, the checkbox does not change. Can anyone tell me the problem?
I think you are running into a state mutation problem which is causing some unexpected behavior. The reason for this is because inside your if statement within map you are not returning your modified array item and you are actually modifying your state array and your new array.
How to fix: return your modified array item inside the if statement
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
return {
todos: updatedTodos
}
})
Or use a one liner with conditional (ternary) operator:
this.setState(prevState => ({
...prevState,
todos: prevState.todos.map(todo => todo.id === id ? { ...todo, ...{ completed: !todo.completed } } : todo)
}))
Look at this example I created in playground to get a better understanding:
I defined two arrays, one array uses map() without the return and the other array is using map() with the return.
Our goal is to keep our two arrays exactly the same and using map() to create a new modified array. Look at the log results and notice how our initial array gets modified aswell. Our second todo item in this array should have a completed value of true but it gets changed to false after our map() which we would want to avoid. By returning our array item in the correct way we avoid this.
To get a better understanding of what state mutation is and how to avoid check this.

state with different name but same array changed

So, I define 2 states with the same array, it's listRasioUom and showListRasioUom. but when I setstate to listRasioUom state, the showListRasioUom state also changed with the same value as listRasioUom. any help? this is my code
handleChange = (e) => {
if (["rasio", "uom"].includes(e.target.name)) {
let listRasioUom = this.state.listRasioUom
if(listRasioUom.some(el => el.uom === e.target.value))
{
alert('Duplicate uom')
}
else
listRasioUom[e.target.dataset.id][e.target.name] = e.target.value;
this.setState({ listRasioUom })
}
}
}
showListRasioUom state is used for fetching the data on my datatables, while listRasioUom state is used to modified the state, so i only want to change listrasiouom state, but when i use setstate on listRasioUom state, the showListRasioUom also change.
<MDBDataTable
info={false}
paging={false}
searching={false}
striped
bordered
small
data={{columns: this.state.columns, rows: this.state.showListRasioUom}}
noBottomColumns
/>
Object and arrayss in javascript are assigned by reference and if you mutate the original array the other array which also holds the same reference is also updated
You should update your data in an immutable manner
handleChange = (e) => {
if (["rasio", "uom"].includes(e.target.name)) {
let listRasioUom = this.state.listRasioUom
if(listRasioUom.some(el => el.uom === e.target.value))
{
alert('Duplicate uom')
}
else {
const id = e.target.dataset.id;
const {name,value} = e.target;
this.setState(prev => ({
listRasioUom: prev.listRasioUom.map((item, i) => {
if(i == id) return {...item, [name]: value};
})
return item;
}))
}
}
}

React state variable updates automatically without calling setState

I am facing the following issue and not able to figure it out.
I have two variables inside the state called userDetails & userDetailsCopy. In componentDidMount I am making an API call and saving the data in both userDetails & userDetailsCopy.
I am maintaining another copy called userDetailsCopy for comparison purposes.
I am updating only userDetails inside setState but even userDetailsCopy is also getting updated instead of have old API data.
Below is the code :
constructor(){
super()
this.state={
userDetails:{},
userDetailsCopy: {}
}
}
componentDidMount(){
// API will return the following data
apiUserDetails : [
{
'name':'Tom',
'age' : '28'
},
{
'name':'Jerry',
'age' : '20'
}
]
resp.data is nothing but apiUserDetails
/////
apiCall()
.then((reps) => {
this.setState({
userDetails: resp.data,
userDetailsCopy: resp.data
})
})
}
updateValue = (text,i) => {
let userDetail = this.state.userDetails
userDetail[i].name = text
this.setState({
userDetails: userDetail
})
}
submit = () => {
console.log(this.state.userDetials) // returns updated values
console.log(this.state.userDetailsCopy) // also return updated values instead of returning old API data
}
Need a quick solution on this.
The problem with this is that you think you are making a copy of the object in state by doing this
let userDetail = this.state.userDetails
userDetail.name = text
But, in Javascript, objects are not copied like this, they are passed by referrence. So userDetail at that point contains the referrence to the userDetails in your state, and when you mutate the userDetail it goes and mutates the one in the state.
ref: https://we-are.bookmyshow.com/understanding-deep-and-shallow-copy-in-javascript-13438bad941c
To properly clone the object from the state to your local variable, you need to instead do this:
let userDetail = {...this.state.userDetails}
OR
let userDetail = Object.assign({}, this.state.userDetails)
Always remember, Objects are passed by referrence not value.
EDIT: I didn't read the question properly, but the above answer is still valid. The reason userDetailCopy is being updated too is because resp.data is passed by referrence to both of them, and editing any one of them will edit the other.
React state and its data should be treated as immutable.
From the React documentation:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Here are five ways how to treat state as immutable:
Approach #1: Object.assign and Array.concat
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = Object.assign({}, userDetails[index]);
userDetail.name = text;
const newUserDetails = []
.concat(userDetails.slice(0, index))
.concat(userDetail)
.concat(userDetails.slice(index + 1));
this.setState({
userDetails: newUserDetails
});
}
Approach #2: Object and Array Spread
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = { ...userDetails[index], name: text };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
}
Approach #3: Immutability Helper
import update from 'immutability-helper';
updateValue = (text, index) => {
const userDetails = update(this.state.userDetails, {
[index]: {
$merge: {
name: text
}
}
});
this.setState({ userDetails });
};
Approach #4: Immutable.js
import { Map, List } from 'immutable';
updateValue = (text, index) => {
const userDetails = this.state.userDetails.setIn([index, 'name'], text);
this.setState({ userDetails });
};
Approach #5: Immer
import produce from "immer";
updateValue = (text, index) => {
this.setState(
produce(draft => {
draft.userDetails[index].name = text;
})
);
};
Note:
Option #1 and #2 only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object.
To maintain the userDetailsCopy unchanged you need to maintain the immutability of state (and state.userDetails of course).
function getUserDerails() {
return new Promise(resolve => setTimeout(
() => resolve([
{ id: 1, name: 'Tom', age : 40 },
{ id: 2, name: 'Jerry', age : 35 }
]),
300
));
}
class App extends React.Component {
state = {
userDetails: [],
userDetailsCopy: []
};
componentDidMount() {
getUserDerails().then(users => this.setState({
userDetails: users,
userDetailsCopy: users
}));
}
createChangeHandler = userDetailId => ({ target: { value } }) => {
const { userDetails } = this.state;
const index = userDetails.findIndex(({ id }) => id === userDetailId);
const userDetail = { ...userDetails[index], name: value };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
};
render() {
const { userDetails, userDetailsCopy } = this.state;
return (
<React.Fragment>
{userDetails.map(userDetail => (
<input
key={userDetail.id}
onChange={this.createChangeHandler(userDetail.id)}
value={userDetail.name}
/>
))}
<pre>userDetails: {JSON.stringify(userDetails)}</pre>
<pre>userDetailsCopy: {JSON.stringify(userDetailsCopy)}</pre>
</React.Fragment>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

React Redux Connect MapState not updating for a filtered item from a collection

I think I've either misunderstood something, or am doing something deeply wrong, when attempting to subscribe to changes on a specific item in a collection in my store. Unless I add a direct list subscription, my component does not receive updates.
The following works:
const mapStateToProps = (state, props) => ({
list: state.podcasts.items,
getItem: props.id
? state.podcasts.items.filter(item => item.clientId === props.id)[0] || {}
: {},
});
If I remove the list item I only receive the the initial state of the collection item I'm subscribing to.
How I'm updating the list in the reducer:
PODCAST_GRADIENT_UPDATED: (state, { payload }) => ({
...state,
items: state.items.map(item => {
if (item.clientId === payload.clientId) {
item.gradient = payload.gradient; // eslint-disable-line
}
return item;
}),
}),
Should the above work without the list subscription?
if not, how should this be done?
this was a rookie error, in that there is some state mutation in the above example. changing my function in the reducer in the following way resolved this:
PODCAST_GRADIENT_UPDATED: (state, { payload }) => ({
...state,
items: state.items.map(item => {
if (item.clientId === payload.clientId) {
return { ...item, gradient: payload.gradient };
}
return { ...item };
}),
}),
notice specifically the use of the spread operator to create and return a new item.

Categories

Resources