How to update single value inside array in redux - reactjs

I am creating a todolist with react and redux and when I update redux state array it doesn't re-render, My state is actually an array which contains objects, something like this:
[{index:1,value:'item1',done:false}, {index:2,value:'item2',done:false}]
What i want to do is on click i want to toggle the value of done to 'true',
But somehow I am unable to do that.
This is what I was doing in my reducer:
list.map((item)=>{
if(item.index===index){
item.done=!item.done;
return [...state,list]
}
But it doesn't re-render even though done keeps changing on clicking the toggle button.
It seems that somehow I am mutating the state.
please tell me where am I going wrong and what should I do instead.
Could you give examples of something similar. I can update state of simple arrays correctly, but doing it for an array containing objects , is what's confusing me.
so, could you give examples of that?
Here's the full reducer code:
export default function todoApp(state=[],action){
switch(action.type){
case 'ADD_TODO':
return [...state,action.item];
case 'TOGGLE_TODOS':
const index = action.index;
const list = state;
list.map((item)=>{
if(item.index===index){
item.done=!item.done;
}
return [...state,list];
});
default:
return state;
}
}

It seems that somehow I am mutating the state.
Correct you are mutating the state, because in js, variable always get reference of object/array. In your case item will have the reference of each object of the array and you are directly mutating the value of item.done.
Another issue is you are not returning the final object properly, also you need to return value for each map iteration otherwise by default it will return undefined.
Write it like this:
case "TOGGLE_TODOS":
return list.map((item) => (
item.index===index? {...item, done: !item.done}: item
))
Or:
case 'TOGGLE_TODOS':
const index = action.index;
const newState = [ ...state ];
newState[index] = { ...state[index], done: !newState[index].done };
return newState;
Full Code:
export default function todoApp(state=[], action){
switch(action.type){
case 'ADD_TODO':
return [...state, action.item];
case 'TOGGLE_TODOS':
const index = action.index;
return state.map((item) => (
item.index===index? {...item, done: !item.done}: item
))
default:
return state;
}
}
Check this snippet:
let list = [
{done:true, index:0},
{done:false, index:1},
{done: true, index:2}
]
let index = 1;
let newList = list.map(item => (
item.index===index? {...item, done: !item.done}: item
))
console.log('newList = ', newList);

Check out the documentation for Array.prototype.Map.
The callback function should return element of the new Array. Try this:
return list.map(item => {
if (item.index === index) {
return {
done: !item.done
...item,
}
return item;
});

Although there already exist two correct answers, I'd like to throw lodash/fp in here as well, which is a bit more dense and readable and also doesn't mutate
import { set } from 'lodash/fp'
return list.map(item => {
if (item.index === index) {
return set('done', !item.done, item)
}
return item
}

Related

React redux not overiding similar object in an array

React redux not overiding similar object in an array.
I was expecting arrray of object with different properties. Also If there was any similar property in array than the count will increase from 1 to onwards
Array.push doesn't create array and you should avoid using it.
And you're not returning new state for Case CARTITEMS
you should always return a new state for each case clause.
Also try to split CARTITEMS case clause into 2 different actions, one to increase the quantity and one for adding new item but remember to avoid Array.push() and instead
return {...state, newPropToOverride }
The solution to the question is
case ADDCARTITEMS:
// adding items to cart and removing duplicates while counting them
const isItem = state.CartItems.find(ci => ci.id === action.payload.id)
if(isItem){
return {
...state,
CartItems: state.CartItems.map(item => {
if(item.id === action.payload.id){
return {...item, count: item.count + 1}
} else {
return item
}
})
}
}
else {
return {
...state,
CartItems: [...state.CartItems, {...action.payload, count: 1}]
}
}

Update value in array in reducer

I have a reducer
const initialState = {
elements: [{"flag": false}, {"flag": false}]
};
const checkReducer = function (state = initialState, action) {
switch (action.type) {
case CHANGE_CHECK:
return {...state, //here I want update element by index, that pass in action};
I need to update an existing value in array elements, that I get by the index passed in action.
I can do it like this
state.elements[action.key] = {"flag": !action.flag}
but then I'll change existing state. according to the redux principles, I can't do it.
So I have to use spread operator and change new object. But I don't know how to use it this way.
Tried something like this
...state, elements[action.index]: {"flag": !action.flag}]
but it isn't worked. Is there a way to do what I want?
return {
...state,
elements: state.elements.map((element, index) => {
if (index === action.key) {
return {"flag": !action.flag}
}
return element
})
}
array#map will create a new array, and change only the item whose index match action.key.
If you find this process tedious, you could use libraries that let mutate your state while keeping the reducer returning new state. One of those is immer.

Change object in array if found by id

According to Redux design patterns if you want to change object in array, you have to use state.items.map, but is it ok to use array.findIndex and if item not found return old state? Is it bad practices, if yes why?
Redux pattern method. Method returns new state, even if room not found.
const roomId = action.payload.room.id;
const roomsList = state.roomsList.map(room => {
if (room.id === roomId) {
return action.payload.room;
} else {
return room;
}
});
return {
...state,
roomsList,
};
Second way, that I like more. Method returns new state only if room with given ID is found
const roomId = action.payload.room.id;
const idx = state.roomsList.findIndex(room => room.id === roomId);
if(idx!==-1) {
const roomsList = Array.from(state.roomsList);
roomsList[idx] = action.payload.room;
return {
...state,
roomsList,
};
}else{
return state;
}
It's always okay to return the previous state if a reducer did not make any changes.
That's what the default case typically does in the usual switch within a reducer function, like in the example from the official redux docs.

Redux reducer, check if value exists in state array and update state

So I've got an array chosenIds[] which will essentially hold a list of ids (numbers). But I'm having trouble accessing the state in my reducer to check whether the ID I parsed to my action is in the array.
const initialState = {
'shouldReload': false,
'chosenIds': [],
};
export default function filter(state = initialState, action) {
switch (action.type) {
case ADD_TYPE:
console.log(state.chosenIds, "Returns undefined???!!!");
// Check if NUMBER parsed is in state
let i = state.chosenIds.indexOf(action.chosenId);
//If in state then remove it
if(i) {
state.chosenIds.splice(i, 1);
return {
...state.chosenIds,
...state.chosenIds
}
}
// If number not in state then add it
else {
state.chosenIds.push(action.chosenId)
return { ...state.chosenIds, ...state.chosenIds }
}
I'm not to sure what's going on...But when I log state.chosenIds, it returns undefined? It doesn't even return the initial empty array [] .
Basically what this function is suppose to do is check to see if the action.chosenId is in the state.chosenIds, If it is, then remove the action.chosenId value, if it's not, then add the action.chosenId to the state.
I'm seeing a few different issues here.
First, you're using splice() and push() on the array that's already in the state. That's direct mutation, which breaks Redux. You need to make a copy of the array, and modify that copy instead.
Second, the object spread usage doesn't look right. You're using it as if "chosenIds" was an object, but it's an array. Also, you're duplicating the spreads. That's causing the returned state to no longer have a field named "chosenIds".
Third, Array.indexOf() returns -1 if not found, which actually counts as "truthy" because it's not 0. So, the current if/else won't do as you expect.
I would rewrite your reducer to look like this:
export default function reducer(state = initialState, action) {
switch(action.type) {
case ADD_TYPE:
let idAlreadyExists = state.chosenIds.indexOf(action.chosenId) > -1;
// make a copy of the existing array
let chosenIds = state.chosenIds.slice();
if(idAlreadyExists) {
chosenIds = chosenIds.filter(id => id != action.chosenId);
}
else {
// modify the COPY, not the original
chosenIds.push(action.chosenId);
}
return {
// "spread" the original state object
...state,
// but replace the "chosenIds" field
chosenIds
};
default:
return state;
}
}
another aproach with a standalone function:
export default function reducer(state = initialState, action) {
switch(action.type) {
case ADD_TYPE:
function upsert(array, item) {
// (1)
// make a copy of the existing array
let comments = array.slice();
const i = comments.findIndex(_item => _item._id === item._id);
if (i > -1) {
comments[i] = item;
return comments;
}
// (2)
else {
// make a copy of the existing array
let comments = array.slice();
comments.push(item);
return comments;
}
}
return {
...state,
comments: upsert(state.comments, action.payload),
};
default:
return state;
}
}

reducer: adding to array data

if i pull some data from an external source fro the initial state, then want to add additional information like for example 'liked'?
i've tried adding to the products array but its go messy, I'm thinking i should have an additional array for liked items then put the product id in this, the only thing is i need it to reflect in the product that it has been liked and I'm mapping the product data to the item.
whats the best way to go about this ?
const initialState = {
isFetching: false,
products: [],
};
should i add favs: [] ?
how would i reflect the liked state to my product as I'm mapping the products array to the product component? and the liked state is now in the favs?
i tried doing this to add it to the product array but it got really messy (something like this)
case ADD_LIKED:
state.products[action.index]['liked'] = true;
return state;
state.products[action.index]['liked'] = true;
The problem here is that you are mutating the state inside the reducer which is one of the things you should never do inside a reducer.
You'll find that writing functions which don't mutate the data are much easier if you break them down into smaller parts. For instance you can start to split your application up.
function productsReducer(products = [], action) {
// this reducer only deals with the products part of the state.
switch(action) {
case ADD_LIKED:
// deal with the action
default:
return products;
}
}
function app(state = {}, action) {
return {
isFetching: state.isFetching,
products: productsReducer(state.products, action)
}
}
In this case I would definitely want to write a little immutability helper.
function replaceAtIndex(list, index, replacer) {
const replacement = replacer(list[index]);
const itemsBefore = list.slice(0, index),
itemsAfter = list.slice(index + 1);
return [...itemsBefore, replacement, ...itemsAfter];
}
You can complement this with a generic function for changing objects in lists.
function updateInList(list, index, props) {
return replaceAtIndex(list, index, item => {
return { ...props, ...item };
});
}
Then you can rewrite your function in the immutable form
switch(action) {
case ADD_LIKED:
return updateInList(products, action.index, { liked: true });
default:
return products;
}
You could even get fancy by partially applying the function. This allows you to write very expressive code inside your reducers.
const updateProduct = updateInList.bind(this, products, action.index);
switch(action) {
case ADD_LIKED:
return updateProduct({ liked: true });
case REMOVE_LIKED:
return updateProduct({ liked: false });
default:
return products;
}

Resources