react setState array of object using map doesn't work? - javascript

I have problem doing a setState changing value of a nested array of object. Below code suppose
to change question of id 2 to answer: true but it did not, what's wrong?
this.state = {
questions: [
{
id: 1,
answer: ''
},
{
id: 2,
answer: ''
},
]
}
//I have a click event somewhere
this.setState(
{
questions: this.state.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
}
} else {
return { ...q }
}
})
},
console.log(this.state.questions[1]) // did not see id of 2 being changed to true?
)

The console.log(this.state.questions[1]) line is executed before the this.setState line is executed, that's why the old state is printed to the console. You should put the line inside a function to delay the execution:
this.setState(..., () => console.log(this.state.questions[1]));
Also it is recommended to use a function as the first argument if the changed state is derived from the current state because React doesn't apply the new state immediately therefore this.state can be outdated when React applies the new state:
this.setState(state => ({
questions: state.questions.map(q => {
if (q.id === 2) {
return {...q, answer: true};
}
return q;
})
}), () => {
console.log(this.state.questions[1]);
});

You are not invoking your setState callback. Try like this:
this.setState(
{
questions: this.state.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
};
}
return { ...q };
})
},
() => console.log(this.state.questions[1]) // did not see id of 2 being changed to true?
);
Though, since you are using the current state to update your state again, it would be better to use functional setState.
this.setState(
currentState => ({
questions: currentState.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
};
}
return { ...q };
})
}),
() => console.log(this.state.questions[1])
);
Also, you don't have to log your state in a callback to setState. You can log your state in your render method without struggling setState's callback.
this.setState(
currentState => ({
questions: currentState.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
};
}
return { ...q };
})
})
);
....
render() {
console.log( this.state );
....
}

I think it's because Array.map returns an array. Try:
this.setState(
{
questions: this.state.questions.map(q => {
if (q.id === 2) {
q.answer = true;
}
return q;
})
},
console.log(this.state.questions[1]) // did not see id of 2 being changed to true?
)

Related

Trying to combine a .forEach and a .map function but I get return undefined

I have the following .map function that creates a new array of elements and changes their selected property to false:
const newAlteredData = alteredData.map((element) => {
return { ...element, selected: false };
});
However, now I want to only change the elements that also exist in another array called changeRows
I tried the following:
const newAlteredData = alteredData.map((element) => {
changeRows.forEach((changeRow) => {
if (changeRow.deviceId === element.deviceId) {
return { ...element, selected: false };
}
return element;
});
});
However, this just return an array of undefined. Am I not using .map and/or .forEach correctly here?
Am I not using .map and/or .forEach correctly here?
forEach() is NOT the right method to use here. It is meant for iterating over an array - not finding elements inside it.
forEach() method doesn't not returns anything. Any value returned from its callback function is ignored.
Javascript provides multiple methods to find an element inside an array.
You can use the find method to find an element in the changeRows array.
const newAlteredData = alteredData.map((element) => {
const exists = changeRows.find(el => el.deviceId === element.deviceId);
if (exists) {
return { ...element, selected: false };
}
});
Other methods that can be used:
findIndex
includes
some
As #sirius mentioned you forgot to return something from the forEach.
I would fix it this way:
const newAlteredData = alteredData.map((element) => {
const arr = []
changeRows.forEach((changeRow) => {
if (changeRow.deviceId === element.deviceId) {
arr.push({ ...element, selected: false });
}
arr.push(element);
});
return arr
});
You can not return any thing inside a forEach.
Change this:
const newAlteredData = alteredData.map((element) => {
changeRows.forEach((changeRow) => {
if (changeRow.deviceId === element.deviceId) {
return { ...element, selected: false };
}
return element;
});
});
TO
const newAlteredData = alteredData.map((element) => {
changeRows.find(changeRow => changeRow.deviceId === element.deviceId) {
return { ...element, selected: false };
}
return element;
});
forEach don't return anything in javascript.
You can use filter to achieve what you want and map through only the existing elements.
const newAlteredData = alteredData.filter(
element => changeRows.find(
changeRow => changeRow.deviceId === element.deviceId
)
).map((element) => {
return { ...element, selected: false };
});

Edit function not saving changes to state data in React

I am trying to provide functionality in my webpage for editing state data.
Here is the state structure
state = {
eventList:[
{
name: "Coachella"
list: [
{
id: 1,
name: "Eminem"
type: "rap"
}
{
id: 2,
name: "Kendrick Lamar"
type: "rap"
}
]
}
]
}
I want to be able to edit the list arrays specifically the id, name, and type properties but my function doesn't seem to edit them? I currently pass data I want to override id name and type with in variable eventData and an id value specifying which row is selected in the table which outputs the state data.
Here is the function code:
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList;
eventListNew.map((event) => {
event.list.map((single) => {
if (single.id == id) {
single = eventData;
}
});
});
this.setState({
eventList: eventListNew,
});
};
When I run the code the function doesn't alter the single map variable and I can't seem to pinpoint the reason why. Any help would be great
edit:
Implementing Captain Mhmdrz_A's solution
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
event.list.map((single) => {
if (single.id == id) {
single = eventData;
}
});
});
this.setState({
eventList: eventListNew,
});
};
I get a new error saying Cannot read property list of undefined in another file that uses the map function to render the state data to the table?
This is the part of the other file causing the error:
render() {
const EventsList = this.props.eventList.map((event) => {
return event.list.map((single) => {
return (
map() return a new array every time, but you are not assigning it to anything;
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
event.list.forEach((single) => {
if (single.id == id) {
single = eventData;
}
});
return event
});
this.setState({
eventList: eventListNew,
});
};
const editPickEvent = (eventData, id) => {
const updatedEventList = this.state.eventList.map(event => {
const updatedList = event.list.map(single => {
if (single.id === id) {
return eventData;
}
return single;
});
return {...event, list: updatedList};
});
this.setState({
eventList: updatedEventList,
});
}
Example Link: https://codesandbox.io/s/crazy-lake-2q6ez
Note: You may need to add more checks in between for handling cases when values could be null or undefined.
Also, it would be good if you can add something similar to the original data source or an example link.
Turns out primitive values are pass by value in javascript, which I didn't know and why the assignment wasn't working in some of the previous suggested answers. Here is the code that got it working for me:
editEvent = (EventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
const newList = event.list.map((single) => {
return single.id == id ? EventData : single;
});
return { ...event, list: newList };
});
this.setState({
eventList: eventListNew,
});
};

how to push into nested array in reducer

Above image shows what happens when I trying to dispatch a reply action.
Currently trying to push the input field value etc into the replyComments...but it is still empty even when dispatching this action.... the reducer is checking the majorIndex first and then the minorIndex, from there it should push it into the replyComments. Any one knows how I can fix this?
Link to working Codesandbox
case types.REPLY_COMMENT: {
return {
...state,
enableToggleComment: true,
imageData: state.imageData.map(image => image.id === action.majorIndex ?
{
...image,
comments: {
[action.minorIndex]: {
replyComments: [...image.comments.replyComments, { comment: action.newComment, likeComment: image.toggle, enableReply: false }]
}
}
} : image
)
}
}
EDIT:
I know noticed it's due to a missing key to [action.minorIndex] and when using the ? operator and want to return an object you should add the parenthesis, otherwise you are declaring a scope (like i did whoops)
case types.REPLY_COMMENT: {
return {
...state,
enableToggleComment: true,
imageData: state.imageData.map(image => image.id === action.majorIndex ?
({
...image,
comments: {
[action.minorIndex]: {
replyComments: [...image.comments[action.minorIndex].replyComments, { comment: action.newComment, likeComment: image.toggle, enableReply: false }],
}
}
}) : image
)
}
}
I've only added action.replyText but you should be able to add the rest now
case types.REPLY_COMMENT: {
return {
...state,
enableToggleComment: true,
imageData: state.imageData.map(image =>
image.id === action.generalIndex
? {
...image,
comments: image.comments.map((comment, i) =>
i === action.majorIndex
? {
...comment,
replyComments: [
...comment.replyComments,
action.replyText
]
}
: comment
)
}
: image
)
};
}

How do I update an array of objects in component state?

I am trying to update the property of an object which is stored in an array.
my state looks something like this:
state = {
todos: [
{
id: '1',
title: 'first item,
completed: false
},
{
id: '2',
title: 'second item,
completed: false
}
],
}
What I am trying to do is access the second element in the 'todos' array and update the completed property to either false -> true or true -> false.
I have a button with the handler for update, and my class method for the update looks like this:
onUpdate = (id) => {
const { todos } = this.state;
let i = todos.findIndex(todo => todo.id === id);
let status = todos[i].completed
let updatedTodo = {
...todos[i],
completed: !status
}
this.setState({
todos: [
...todos.slice(0, i),
updatedTodo,
...todos.slice(i + 1)
]
});
}
While this does work, I want to find out if there is a more concise way of achieving the same result; I tried to use Object.assign(), but that didn't work out because my 'todos' is an array, not an object. Please enlighten me with better code!
It would be best to use update function to make sure you don't work on outdated data:
onUpdate = (id) => {
this.setState(prevState => {
const copy = [...prevState.todos];
const index = copy.findIndex(t => t.id === id);
copy[index].completed = !copy[index].completed;
return { todos: copy }
})
}
You can simply copy your todos from state, then make edits, and after that put it back to the state
onUpdate = (id) => {
var todos = [...this.state.todos]
var target = todos.find(todo => todo.id == id)
if (target) {
target.completed = !target.completed
this.setState({ todos })
}
}

Using spread operator to setState in multiple succession

I have some values stored in local storage. When my component mounts, I want to load these values into the state. However, only the last property being added is added to the state. I've checked the values on my localStorage, and they are all there. Furthermore, when I log the variables (desc, pic or foo) in the condition block, they are there.
I thought at first each subsequent if block is re-writing the state, but this is not the case as I am using the spread operator correctly (I think!), adding the new property after all pre-existing properties.
I think the problem is that the code in the last if block is running before the state is set in the first if block. How do I write the code so I get all three properties from my local storage into the state?
//what I expect state to be
{
textArea: {
desc: 'some desc',
pic: 'some pic',
foo: 'some foo'
}
}
//what the state is
{
textArea: {
foo: 'some foo'
}
}
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
if (desc) {
console.log(desc) //'some desc'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
desc: desc,
},
}, ()=>console.log(this.state.textArea.desc)); //undefined
}
if (pic) {
console.log(pic) //'some pic'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
pic: pic,
},
}, ()=>console.log(this.state.textArea.pic)); //undefined
}
if (foo) {
console.log(foo) //'some foo'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
foo: foo,
},
}, ()=>console.log(this.state.textArea.foo)); //'some foo'
}
}
You are likely being caught by React batching setState calls by shallow-merging the arguments you pass. This would result in only the last update being applied. You can fix this by only calling setState once, for example:
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
this.setState({
textArea: Object.assign({},
desc ? { desc } : {},
pic ? { pic } : {},
foo ? { foo } : {}
)
});
}
The other version is to pass an update function to setState rather than an update object, which is safe to use over multiple calls. The function is passed two arguments: the previous state, and the current props - whatever you return from the function will be set as the new state.
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
this.setState(prevState => {
if (desc) {
return {
textArea: {
...prevState.textArea,
desc
}
}
} else {
return prevState;
}
});
// Repeat for other properties
}
It's a little more verbose using this approach, but does offer the opportunity to extract state updating functions outside of your component for testability:
// Outside component
const updateSubProperty = (propertyName, spec) => prevState => {
return {
[propertyName]: {
...prevState[propertyName],
...spec
}
}
}
const filterNullProperties = obj => {
return Object.keys(obj).reduce((out, curr) => {
return obj[curr] ? { ...out, [curr]: obj[curr] } : out;
}, {});
}
componentDidMount () {
this.setState(updateSubProperty("textArea",
filterNullProperties(
desc: window.localStorage.getItem('desc'),
pic: window.localStorage.getItem('pic'),
foo: window.localStorage.getItem('foo')
)
));
}
This way adds some complexity, but (in my opinion) gives a really readable component where it is clear to our future selves what we were trying to achieve.

Categories

Resources