How to update a state which is a array of objects? - reactjs

My state is as follows
this.state = {
todos: [{
title: 'asas',
status: 'incomplete',
uuid: 11
}, {
title: 'asas',
status: 'incomplete',
uuid: 12
}, {
title: 'asas',
status: 'complete',
uuid: 13
}],
currentTab: "Show All"
}
and whenever a user clicks on any of the todo items's checkBox i want to update the state status of the checkbox and i have written the following code for it
this.state.todos.map(todo => {
if (todo.uuid === uuid) todo.status = (todo.status === 'complete') ? 'incomplete' : 'complete'
});
this.forceUpdate();
Is Using forceUpdate a good approach here? as i have updating only a single value inside an array of objects. Is there a better solution for this problem?

either of the following will call setState with the updated state without modifying the current state.
https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#inserting-and-removing-items-in-arrays
using the spread operator:
edit.. actually, this is the hard way 8)
see https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#updating-an-item-in-an-array
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return {
todos: [
...prevState.todos.slice(0, idx),
{
...prevState.todos[idx],
status: prevState.todos[idx].status === "complete" ? "incomplete" : "complete",
}
...prevState.todos.slice(idx + 1),
]
}
});
or using immer:
import produce from "immer";
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return produce(prevState, draft => {
draft.todos[idx].status = prevState.todos[idx].status === "complete" ? "incomplete" : "complete"
});
});

Related

How do i stop a dependency from re-rendering infinite times in a useEffect?

I have a react-select multiselect component which is required to have preselected values based on the user role. The react-select component is used to filter data in a react-table.
I have 2 user roles - Dev and tester.
If it the dev, I need to have open and Reopened issues to be filtered on load
If it is a tester, I need to have resolved issues on load
This is a part of the code that i am using to achieve preselected
async function loadInfo() {
const body = {
project_id: projDetails.id,
};
const response = await axios({
method: "post",
url: apilist.dropdownData,
data: body,
})
.catch((err) => console.log(err));
if (response) {
const getData = response.data.data;
// console.log("IsGeneralInfo:", getData)
setGeneralInfo(getData);
let filteredenv = getData.status.filter((item) => item.id == 8 || item.id == 6)
let envfiltered = filteredenv.map((k) => {
return ({ label: k.values, value: k.values });
})
// console.log("envfilter", envfiltered);
// handleMultiStatusFilter(envfiltered);
}
}
// const {current:myArray}=useRef([{ label: 'Closed', value: 'Closed' }])
useEffect(() => {
if(envfilter){
let myArray=[{ label: 'Closed', value: 'Closed' },{ label: 'Reopen', value: 'Reopen' }];
handleMultiStatusFilter(myArray);
}
}, [selectedOptions])
const handleStatusFilter = (e) => {
setFilterValue(e);
if (e.length > 0) {
dispatch(filterByValue({ type: e, viewIssue: viewIssue, }))
}
else {
dispatch(showAllStatus({ type: 'All', viewIssue: viewIssue, }))
}
}
const handleMultiStatusFilter = (e) => {
setFiltered([])
let arr = []
e.map((data) => {
setFiltered(prevState => [...prevState, data.value]);
arr.push(data.value);
})
setSelectedOptions(e)
handleStatusFilter(arr)
}
This is a part of the redux code used for filtering
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchIssueList.fulfilled, (state, action) => {
// Add user to the state array
state.issuesList = {
status: 'idle',
data: action.payload.data.data !== undefined ? action.payload.data.data : [],
dataContainer: action.payload.data.data !== undefined ? action.payload.data.data : [],
no_of_records: action.payload.data.data !== undefined ? action.payload.data.data.length : 0,
error: {}
}
})
The code works fine with the filtering once i login, but the rerendering keeps going to infinite loop
Is there any way i could stop the infinite rerendering of code and have the filtering happen on load of the screen?
while working with dependencies in useEffect, try to use the most primitive part you can find. no complex objects, as they change way too fast.
for example: use the length of an array not the array itself.
even though for arrays it's mostly safe to use itself.
sorry. correction: for arrays it's not safe either. complex objects are compared by reference not by value. for that you need primitive types like number, string or boolean.

Update nested React state?

I'm trying to update part of a state object that is nested. This is the object:
const [buttonObject, setButtonObject] = useState({
intro: [
{
id: '123',
name: 'first_intro_name',
selected: false,
},
{
id: '124',
name: 'second_intro_name',
selected: false,
},
],
experience: [
{
id: '789',
name: 'first_experience_name',
selected: false,
},
{
id: '8910',
name: 'second_experience_name',
selected: false,
},
],
});
When a button is clicked I want to toggle the selected state. I'm using a click handler that looks like this:
const handleButtonClick = ({ id, selected }) => {
if (id === '123') {
buttonsObject.intro.map(
pref => (pref.selected = pref.id === id ? !pref.selected : selected)
);
setButtonsObject(buttonsObject);
} else if (id === '124') {
buttonsObject.intro.map(
pref => (pref.selected = pref.id === id ? !pref.selected : selected)
);
setButtonsObject(buttonsObject);
}
};
It would handle experiences as well. The issue is that right now it seems like rather than updating the object it just overwrites the object or creates a new one. It also doesnt pass that information back down to the component even though I have it routed correctly so it should.
Is there better/correct syntax for updating nested state like this?
Thanks.
instead of checking again with if condition use higher order array function and spread operator to get optimal solution.
setButtonObject(previous => {
return {
...previous,
info: previous.info.map(item => item.id === id ? {
...item,
selected: true
} ? item)
}
})

Functional component problems React

I transformed a class component into a functional component but it looks like it does not work in a way it suppose to work and I can not find what is wrong. When I create a new object there is no name for the object and when I try to mark the object as a complete it removes all created objects at ones. I created a codesandbox here. Unfortunately, I am not too much familiar with functional component. Any help would be appreciated.
Here is my codesandbox sample:
https://codesandbox.io/s/crazy-sid-09myu?file=/src/App.js
Your Todos:
const [todos, setTodos] = useState([
{ id: uuid(), name: "Task 1", complete: true },
{ id: uuid(), name: "Task 2", complete: false }
]);
onAddHandler:
const addTodo = () =>
setTodos([...todos, { id: uuid(), name: "New Task", complete: false }]);
onSetCompleteHandler:
const setCompleteHandler = id =>
setTodos(
todos.map(todo => {
if (todo.id === id) {
return {
...todo,
complete: todo.complete ? 0 : 1
};
}
return todo;
})
);
I have created your new todos. Check out this link
Todos App
I have updated your code, please check the URL https://codesandbox.io/s/determined-morning-n8lgx?file=/src/App.js
const onComp = id => {
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
let t = { ...todos[i] };
t.complete = !t.complete;
todos[i] = t;
}
}
setTodos([...todos]); // Here todos reference need to be changed
};
And also
const onSubmit = event => {
event.preventDefault();
setTodos([
...todos,
{
id: generateNewId(),
name: newTodoName,
complete: false
}
]);
setNewTodoName("");
};
While using hooks we need to be careful about state variable updates. While manipulating arrays and objects use the spread operator to create new references which invokes child components and re-render current component.

Keep checkbox state across pagination vue-bootstrap

I am using a bootstrap table using pagination to display users. On every row a checkbox for selecting more than one at a time to perform an action.
I manage to write the functions to select one and select all, keeping the array across pagination.
But now I am not able to control the ui while navigating across pages using pagination. As I said, the array I created for storing the selected users stays across pages, but I 'loose the tick' on the checkboxes... It looks like the v-model binding gets lost while surfing the pages...
Here the inherent parts of the code, hoping someone could help.
In the template:
<b-table
v-if="users && users.length > 0 && !isLoading"
id="table-transition-userList"
:key="users.id"
responsive
:tbody-transition-props="transProps"
:fields="fields"
:items="users"
hover
class="mx-2 py-1 tbl-user-list"
:per-page="perPage"
:filter="filter"
:filterIncludedFields="filterOn"
#filtered="onFiltered"
>
<template
v-slot:head(checkbox)="data"
>
<b-form-checkbox
size="lg"
v-model="isSelectedAll"
#change="selectAll(data)"
/>
</template>
<template
v-slot:cell(checkbox)="data"
>
<b-form-checkbox
v-model="data.item.selected"
#change="select(data.item)"
/>
</template>
In the script data:
fields: [
{ key: 'checkbox', label: ''},
{ key: 'fullName', label: 'Utente' },
{ key: 'user.username', label: 'Username' },
{ key: 'user.email', label: 'Email' },
{ key: 'buttons', label: 'Operazioni' }
],
totalRows: 1,
currentPage: 1,
perPage: 5,
pageOptions: [5, 10, 15, 20, 25],
filter: null,
filterOn: [],
users: [],
selectedUsers: [],
isSelectedAll: false
In the methods:
selectAll() {
this.isSelectedAll = !this.isSelectedAll
this.users.forEach(user => user.selected = this.isSelectedAll)
const notSelected = this.users.filter(user => !user.selected)
const selected = this.users.filter(user => user.selected)
// new selected users array without current page selected users
let selectedUsersCopy = this.selectedUsers.slice().filter(user =>
!notSelected.find(e => e.id === user.id)
)
if(notSelected.length > 0) {
this.isSelectedAll = true
}
else {
this.isSelectedAll = false
}
this.selectedUsers = [
...selectedUsersCopy,
...selected.filter(user => !selectedUsersCopy.find(e => e.id === user.id))
]
},
select(user) {
user.selected = !user.selected
this.isSelectedAll = false
const selected = this.users.filter(user => user.selected)
if(selected.length === this.users.length) {
this.isSelectedAll = true
}
else {
this.isSelectedAll = false
}
let isDouble = false
if(this.selectedUsers.find(v => v.user.id === user.id)) isDouble = true
if(user.selected) {
if(isDouble) {
console.log('double if user selected and isDouble', isDouble)
console.log("object already exists", this.selectedUsers)
return
}
else {
this.selectedUsers.push(user)
return this.selectedUsers
}
}
else {
const index = this.selectedUsers.indexOf(user);
this.selectedUsers.splice(index, 1);
console.log("removed, new array: ", this.selectedUsers)
}
},
async pagination(page) {
const payload = {
limit: this.perPage,
offset: (page - 1) * this.perPage
}
this.isLoading = true
this.isSelectedAll = false
await this.$store.dispatch('user/setUsers', payload)
const response = this.$store.getters.users
this.users = response.results
this.isLoading = false
this.currentPage = page
}
My pagination is calling one api with different limits and offsets.
I think the problem is to be found in the built-in checked property of the form checkbox though...
Thanks to anyone who could give me a hint.
xx
Is your store dispatch event doing an axios call to fetch the page of data? Then the row's checkbox state will be what ever is returned from the call to the remote server (in which case it is being reset).
Once you receive your paged data, you need to iterate over it and set the selected value to true or false, based on the existence in your selectedUsers array, before passing it to <b-table>
async pagination(page) {
const payload = {
limit: this.perPage,
offset: (page - 1) * this.perPage
}
this.isLoading = true
this.isSelectedAll = false
await this.$store.dispatch('user/setUsers', payload)
const response = this.$store.getters.users
// Restore the selected state before passing the array to b-table
this.users = response.results.map(user => {
// if the user is found in selectedUsers array, then set
// set the selected state to `true`, otherwise `false`
// to restore the selected state
user.selected = !!this.selectedUsers.find(u => u.id === user.id)
return user
})
this.isLoading = false
this.currentPage = page
}
I believe you may have an error in this line as well:
if (this.selectedUsers.find(v => v.user.id === user.id)) isDouble = true
I am thinking it should be:
if (this.selectedUsers.find(u => u.id === user.id)) isDouble = true

updating the state based on condition and adding new property finally retrieving the updated state

whenever the button is clicked i am passing the id , so my state is having three objects
state = items:[{
id:1,
material: 'iron'
},
id:2,
material: 'steel'
},
id:3,
material: 'coal'
}]
//reducer.js
case actionTypes.UpdateMaterialToShow:
return {
...state,
items: state.items.map(obj => {
if(obj.id === action.payload.id){
obj.['show'] = action.payload.status
}
})
}
so whenever the method is invoked i am passing the id and status, so i need to add the property to the state.items
expected output is if the clicked button is iron
state = items:[{
id:1,
material: 'iron',
status: true
},
id:2,
material: 'steel'
},
id:3,
material: 'coal'
}]
how can i get back the updated state as shown above without mutating the state
When you use Array.map() a new array is created. Whenever the object's id is equal to the payload's id, you need to create a new object based on the old one, with the updated properties.
You can use Object.assign() or object spread (like the example) to create the new object:
case actionTypes.UpdateMaterialToShow:
return {
...state,
items: state.items.map(obj => {
if (obj.id === action.payload.id) {
return { ...obj,
status: action.payload.status
};
}
return obj;
})
}
You can use Reactjs#spread-attributes.
return {
...state,
items: state
.items
.map(obj => {
if (obj.id === action.payload.id) {
return {
...obj,
status: action.payload.status
}
} else {
return obj;
}
})
}

Resources