Add item in array with index - react redux and redux-saga - arrays

I try to update my state when adding an item in array, using react/redux and redux-saga middleware. But where I do this, I have an array, where each item have an index (0, 1, 2....) but when I add the new item (named step), the new item doesn't have an index but it is like :
[
0: item1,
1: item2,
2: item3,
step: newItem
]
And I want that the new item have an index like 3: newItem
Here is the reducer :
export function stepReducer(state = [], action) {
switch (action.type) {
case Actions.CREATE_STEP_REQUEST:
return state;
case Actions.STEP_CREATED:
let newArray = action.steps.slice();
newArray.splice(action.index, 0, action.step);
return newArray;
case Actions.FETCH_TRAVEL_STEPS_REQUIRED:
return state;
case Actions.TRAVEL_STEPS_DATA_SUCCESS:
let updatedSteps = _.merge(action.steps, state.steps);
return updatedSteps;
default:
return state;
}
}
and the saga file :
export function* createStep(action) {
const step = yield call(stepApi.createStep, action.travelId, action.step, action.authToken);
const steps = yield select((state) => state.steps);
const index = steps.length;
yield put({ type: Actions.STEP_CREATED, steps, step, index });
}
if someone should help,
thank !

It seems that you are working with lodash, note that _.merge works with objects not arrays.
If you just want to add a new item to the END of an array you can use the ES2015 spread feature:
const nextState = [...state, action.step];

Related

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.

How to update single value inside array in redux

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
}

React Redux reducer is overwriting values LOAD_SUCCESS?

I have a profilesReducer that I want to use to store 1 or more user profiles in my redux store. As an example think Twitter, which needs to store my profile as well as other profiles.
Here is my profilesReducer.js:
import * as types from '../actions/actionTypes';
const initialState = []
export default function profilesReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_PROFILE_SUCCESS:
return [Object.assign({}, action.profile)]
case types.UPDATE_PROFILE_SUCCESS:
return [
...state.filter(profile => profile.id !== action.profile.id),
Object.assign({}, action.profile)
]
default:
return state;
}
}
The problem is LOAD is receiving more than one profile (distinguished by profile.id) but is overwriting the existing profile in the store instead of appending/updating.
I need LOAD_PROFILE to allow for more than 1 user's profile in the store. Any suggestions?
Spread existing state and just add new profile at the end:
case types.LOAD_PROFILE_SUCCESS:
return [...state, action.profile]
Object.assing({}, action.profile) is unnecessary, since all that it does here is copying action.profile into a new empty object {} and returns it.
Pretty sure your code needs to change to this.
import * as types from '../actions/actionTypes';
const initialState = []
export default function profilesReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_PROFILE_SUCCESS:
return [...Object.assign({}, action.profile)]
case types.UPDATE_PROFILE_SUCCESS:
return [
...state.filter(profile => profile.id !== action.profile.id),
Object.assign({}, action.profile)
]
default:
return state;
}
}
The way you had it, you were simply returning an array containing this one item in action.profile, but you need to add your item in to the array that already has the other items. You also cannot simply push as that would mutate the array. This line is all you need to change.
return [Object.assign({}, action.profile)]
Does not append
return [...Object.assign({}, action.profile)]
does append and does not mutate.
You can achieve what you want with either using Object.assign or the spread operator syntax, You need not use both
With Object.assign,
case types.LOAD_PROFILE_SUCCESS:
return Object.assign([], state, action.profile)
With Spread operator syntax
case types.LOAD_PROFILE_SUCCESS:
return [...state, action.profile]
However React docs advise you to use Spread syntax over Object.assign, See this post:
Using Object.assign in React/Redux is a Good Practice?
Edit
I have changed my solution, it includes a way to update an array.
Here is a CodeSandbox to test: https://codesandbox.io/s/zk9r6k1z4p
import * as types from '../actions/actionTypes';
const initialState = []
export default function profilesReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_PROFILE_SUCCESS:
return [...state, action.profile]
case types.UPDATE_PROFILE_SUCCESS:
return state.map( (profile) => {
if(profile.id !== action.profile.id) {
return profile;
}
return {
...profile,
...action.profile
};
});
default:
return state;
}
}

Object order changes after reducer updates the state

Currently I'm learning Redux, playing around with different stuff.
So I made a simple TODO app with possibility to edit each TODO item.
But for some reason UPDATE_TODO reducer puts the updated TODO item at the and of the list. So the item is successfully updated but it jumps to the end of TODO's list, instead of staying at the original position.
Original TODO items positioning:
item 1 <-- being updated
item 2
item 3
Positioning after updating of item 1:
item 2
item 3
item 1 <-- was updated
In my reducer I filter all TODOs except updated one with filter() and then set a new state for the updated TODO item.
Need an advice how to update TODO item state properly so it remains at the original position?
reducers
import { UPDATE_TODO } from '../constants';
const initialState = {
all: [] // array of all TODO's
}
export default function(state = initialState, action) {
switch (action.type) {
...
case UPDATE_TODO:
return {
...state,
all: [
...state.all.filter(todo => todo.id !== action.payload.id),
action.payload
]
};
default:
return state;
}
}
Because you are putting the updated item action.payload in the last:
all: [
...state.all.filter(todo => todo.id !== action.payload.id),
action.payload
]
Here filter will return all the todo items other than the updated one, then after all the items you are putting the action.payload (the updated one).
Instead of that use map and put the condition, when todo.id == action.payload.id will be true then return the updated one otherwise return the existing items.
Like this:
case UPDATE_TODO:
return {
...state,
all: [
...state.all.map(todo => {
if(todo.id == action.payload.id)
return action.payload;
return todo;
})
]
};

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;
}
}

Resources