Object order changes after reducer updates the state - reactjs

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

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

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
}

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

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

Create reducer to update state for different components in redux

I am creating this UI using react and redux. Below is the image of the UI.
This is image of UI
Now it currently displays the data when 'Categories' is clicked. Then when I click on unmapped link ,it shows unmapped products of 'Categories'. But when I click on 'Addons' and then on unmapped link ,it still shows the unmapped products of 'Categories'. How do I write the reducer so that it shows the unmapped products of 'Addons' as well. In my react main file I use (this.props.menu_items) that's why it shows the data of 'Categories' when clicked unmapped after Addons.
This is my reducer file.
import { AppConstants } from '../constants';
const initialState = {
state: [],
menu_items: [],
menu_items_copy: [],
addon_items: [],
unmapped: false,
};
export default (state = initialState, action) => {
switch (action.type) {
case AppConstants.getMenuItems:
return {
...state,
}
case AppConstants.getMenuItemsSuccess:
return {
...state,
menu_items: action.menu_items,//This contains the data showed when
// 'categories' is clicked
menu_items_copy: action.menu_items,
unmapped: false,
}
case AppConstants.getAddonsItems:
return {
...state,
}
case AppConstants.getAddonsItemsSuccess:
return {
...state,
menu_items: action.addon_items,//Here I update the data to addons data
}
case AppConstants.showAllProducts:
return {
...state,
menu_items: action.menu_items,
unmapped: false
}
case AppConstants.getUnmappedMenuItemsSuccess:
return {
...state,
unmapped: true,
menu_items: state.menu_items_copy.filter((item) => {-->How do I write
here for action.addon_items also
return (item.productList.length == 0)
})
}
case AppConstants.hideError:
return {
...state,
error: null
}
default:
return state
}
};
The only state I would change via reducer when unmapped is clicked would be to set unmapped: true / false.
There is no need to filter the items array and store it back into the state, as you already have all the info you need to derive your combined items at the point where you pass it to the view.
Have a read about derived state and selectors here http://redux.js.org/docs/recipes/ComputingDerivedData.html
In order to do this you would use a selector to combine the relevant parts of your state to produce a single list of items that is derived from both the items arrays, depending on the unmapped flag. The reselect library is a great helper to do this while memoising for performance, it will only recompute if any of the inputs change, otherwise returns the previously cached value
import {createSelector} from 'reselect'
const selectMenuItems = state => state.menu_items
const selectAddonItems = state => state.addon_items
const selectUnmapped = state => state.unmapped
const selectItemsToShow = createSelector(
selectMenuItems,
selectAddonItems,
selectUnmapped,
(menuItems, addonItems, unmapped) => {
// if unmapped is set - combine all items and filter the unmapped ones
if (unmapped) {
return menuItems.concat(addonItems).filter(item => !item.productList.length)
}
// else just return the menu items unfiltered
return menuItems
}
)
// this selector can then be used when passing data to your view as prop elsewhere
// or can be used in `connect` props mapping as in the linked example)
<ItemsList items={selectItemsToShow(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