This is a follow up question to this question:
Why calling react setState method doesn't mutate the state immediately?
I got a React component with a form which can be used to add items or edit a current item. The form is being saved as a state of the component along with all its values.
When submitting the form I'm doing this:
const onSubmitForm = () =>
{
if(editedItem) //the item to edit
{
EditSelectedItem();
setEditedItem(undefined);
}
else
{
//handle new item addition
}
clearFormValues();
setEditedItem(undefined);
}
And the edit method:
const EditSelectedItem = () =>
{
setItemsList(prevItemsList =>
{
return prevItemsList.map(item=>
{
if(item.id !== editedItem.id)
{
return item;
}
item.name = formSettings["name"].value ?? "";
item.description = formSettings["description"].value ?? "";
item.modified = getNowDate();
return item;
});
})
}
The problem is that because the setItemsList is not being called synchronously, the clearFormValues(); in the submit form method is being called before, and I lose the form's old values (in formSettings)..
How can I keep the old values of formSettings when the setItemsList is called?
The solution is easy here, you can store the formValues in an object before using it an setItemsList
const EditSelectedItem = () =>
{
const values = {
name: formSettings["name"].value ?? "";
description: formSettings["description"].value ?? "";
modified: getNowDate();
}
setItemsList(prevItemsList =>
{
return prevItemsList.map(item=>
{
if(item.id !== editedItem.id)
{
return item;
}
return {...item, ...values};
});
})
}
Related
I created a simple to-do list in ReactJS. It loads components for to-do items stored in a file "TodoData.js", data is stored as followed in that file:
const todosData = [
{
id:1,
text: "Take out the thrash",
completed: true
},
{
id:2,
text: "Grocery shopping",
completed: false
},
App.js uses a TodoItem.js component to render each to-do item with a map function. TodoItem.js uses conditional rendering:
if (props.item.completed == true) {
return (
<div className="todoclassDone">
<input type="checkbox"
onChange={ () => props.handleChange(props.item.id)}/>
<p className="lalatext"><del>{props.item.text}</del></p>
</div>
)
}
else { .... //same code as above but with other className.
Within App.js I use the TodoItem.js component to render each item in TodoData with a map function; if the data.completed = true background is green, else background is red.
Problem: However, the handleChange(id) function in App.js is not working properly. I loop through all objects in todosData; if the id is similar to the id of checkbox which the user clicked it should change to the opposite boolean value using todo.completed = !todo.completed However, when running this code nothing is happening. The handleChange function:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id == id) {
todo.completed = !todo.completed;
}
return todo
})
Extra info: Above mentioned problem is especially weird because if I change the boolean value of the checkbox clicked by the user to either false or true it does work. This does not result in the desired behaviour because now I am only able to change the todo.completed once from false to true. ; In this case the handleChange function would look as follows:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id == id) {
todo.completed = true;
}
return todo
})
Any help is highly appreciated, thanks in advance! :-)
Ciao, you could try to copy state on an array, modify array and set the state wht updated array. Something like:
handleChange(id) {
let result = this.state.todos;
result = result.map(todo => {
if (todo.id === id) todo.completed = !todo.completed;
return todo;
})
this.setState({todos: result})
}
You should return the new state in your setState callback, with the new state object.
Example todo component with relevant code:
class Todo extends Component {
state = {
todos: todosData,
}
handleChange(id) {
this.setState(prevState => {
const todos = prevState.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo;
});
return { todos };
}
}
I got a component which has a form to add a new item and its supposed to also update an existing item. I'm trying to set the value of the form fields such that if a user chooses to edit an item, he will have all of the data of the existing item already in the form, which he just needs to edit.
I'm using useEffect for that:
useEffect(() => {
if(props.editedItem)
{
inputChangedHandler(props.editedItem.companyName, "company");
inputChangedHandler(props.editedItem.name, "name");
inputChangedHandler(props.editedItem.description, "description");
}
}, [props.editedItem])
the method inputChangedHandler is setting the form value of a specific field (company, name, description):
const inputChangedHandler = (newVal, inputIdentifier) =>
{
const updatedOrderForm = {
...formSettings
};
const updatedFormElement = {
...updatedOrderForm[inputIdentifier]
};
updatedFormElement.value = newVal;
updatedOrderForm[inputIdentifier] = updatedFormElement;
setFormSettings(updatedOrderForm);
}
The problem here is that only the last field is changed (description in the case of the code). If I changed the lines order and the "name" will be the last, the name info will appear and not the description.
How can I fix it?
You may be overriding your form with staled values (due to closures).
// closure on `updatedOrderForm` value, staled state
const updatedFormElement = {
...updatedOrderForm[inputIdentifier]
};
Try using functional update which provides the most updated state.
const inputChangedHandler = (newVal, inputIdentifier) => {
setFormSettings((prev) => {
const updatedOrderForm = {
...formSettings,
};
const updatedFormElement = {
...updatedOrderForm[inputIdentifier],
value: newVal,
};
return { ...prev, [inputIdentifier]: updatedFormElement };
});
};
Not sure how is your inputChangedHandler method definition is but you can take the value and use the respective hook to set it.
function inputChangedHandler({companyName,name, description}){
setName(name);
setCompanyName(companyName);
...
}
useEffect(() => {
if(props.editedItem)
{
inputChangedHandler(...props.editedItem);
}
}, [props.editedItem])
I have a group of checkboxes, whenever I select a checkbox I need to push an array of data, like { 'index':1, 'photo':'sample.png' } to state, and whenever I unselecting the checkbox, I need to remove it from the state. after I need to loop through the state to get index and photo to be used
handleSelection = async (media, index, isSelected) => {
alert(index);
if (isSelected == true) {
this.state.mediaSelected.push(media.photo);
} else {
this.state.mediaSelected.splice(this.state.mediaSelected.indexOf(media.photo), 1);
}
console.warn(this.state.mediaSelected);
}
this is working for single value without the key, is there any way to push it with key and value?
You should always update state with this.setState in your case would be something like this:
handleSelection = async (media, index, isSelected) => {
alert(index);
if (isSelected == true) {
this.setState({
mediaSelected: this.state.mediaSelected.push({
index,
photo: media.photo
})
});
} else {
this.setState({
mediaSelected: this.state.mediaSelected.splice(this.state.mediaSelected.indexOf(media.photo), 1)
});
}
console.warn(this.state.mediaSelected);
}
Try this:
Sorry I am working as well as answering your question so it is taking time.
handleSelection = async (media, index, isSelected) => {
let selectPhotosObj = this.state.selectPhotosObj || [];
if (isSelected == true) {
const data = { index, photo: media.photo };
//this.state.selectedPhotoObj will be the container for your object.
selectPhotosObj.push(data)
//need to set the new Array of Object to the state.
this.setState({ mediaSelected: media.photo, selectPhotosObj });
} else {
const removedPhoto = this.state.mediaSelected.filter(value => value !== media.photo);
selectPhotosObj = this.state.selectedPhotosObj.filter(value => value.index !== index);
this.setState({
mediaSelected: removedPhoto,
selectPhotosObj
})
}
console.warn(selectPhotosObj);
}
I am stuck with an ugly issue which I am unable to resolve. I am beginner in React.
This is my Code
handleCheckChildElement(event) {
let items = this.state.items;
items.forEach(items = () => {
if(items.value === event.target.value) {
items.isChecked = event.target.checked;
}
});
this.setState({ items });
}
This is the image of the error -
Use below code for line #55 :
let {items}= {...this.state};
Read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring
Your code can be improved to something like below. Please find relevant comments in the code below for your better understanding
handleCheckChildElement(event) {
const { items } = this.state; //extract state values like this to a const variable
const newItems = items.map(item => { //do map on items because map returns a new array. It’s good practice to use .map than forEach in your case
if(item.value === event.target.value) {
item.isChecked = event.target.checked;
return item; //return updated item object so that it will be pushed to the newItems array
}
return item; // return item because you need this item object as well
});
this.setState({ items: newItems}); //finally set newItems array into items
}
handleCheckChildElement(event) {
const items = this.state.items;
const filtered = items.filter(item => item.value === event.target.value)
.map(item => item.isChecked = event.target.checked) ;
this.setState({items : [...filtered] );
}
I have a unit test that is producing something I didn't expect:
Background: I'm making a simple todo list with Angular/test driven development.
Problem: When I call editTask on an item in the array, it changes the item's value. But, I don't see how it's changed in the original array because the original array is never accessed in the method I'm testing. Please help me connect HOW the original array is being changed? It seems Object.assign is doing this, but why?
describe('editTask', () => {
it('should update the task by id', () => {
const dummyTask1 = { id: 1, name: 'test', status: false };
service.tasks.push(dummyTask1); //refers to TestBed.get(TaskService)
const id = 1;
const values = { name: 'cat', status: false };
service.editTask(id, values);
console.log(service.tasks); // why does this log this object? [Object{id: 1, name: 'cat', status: false}]
expect(service.tasks[0].name).toEqual(values.name); // Test passes
});
});
Here is the method I'm testing:
editTask(id, values) {
const task = this.getTask(id);
if (!task) {
return;
}
Object.assign(task, values); //How does this line change the array?
return task;
}
getTask(id: number) {
return this.tasks.filter(task => task.id === id).pop(); //is this altering the original array somehow?
}
If needed, here's the full Angular service:
export class TaskService {
tasks: any = [];
lastId = 0;
constructor() { }
addTask(task) {
if (!task.id) {
task.id = this.lastId + 1;
}
this.tasks.push(task);
}
editTask(id, values) {
const task = this.getTask(id);
if (!task) {
return;
}
Object.assign(task, values);
return task;
}
deleteTask(id: number) {
this.tasks = this.tasks.filter(task => task.id !== id);
}
toggleStatus(task) {
const updatedTask = this.editTask(task.id, { status: !task.status});
return updatedTask;
}
getTasks() {
return of(this.tasks);
}
getTask(id: number) {
return this.tasks.filter(task => task.id === id).pop();
}
}
Here is the github repo: https://github.com/capozzic1/todo-tdd
The getTask() method is getting a reference to the item in the array using the array filter() method.
It then uses Object.assign() to change the properties of the item. The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
So now the values of the reference in memory of the item is changed. Because it is a reference in memory you will see the original item being changed.