This is one of my redux reducers and I feel it looks very ugly. Is it possible to improve it?
The goal that I want to achieve is very simple:
If I already have this item in my current state, increase the quantity by 1,
otherwise add this item to state.
function globalReducer(state = initialState, action) {
switch (action.type) {
case ADD_TO_CART: {
let { item } = action;
if (state.getIn(['sideCart', 'orderItems', item.id])) {
item.quantity = state.getIn(['sideCart', 'orderItems', item.id]).get('quantity') + 1;
} else {
item.quantity = 1;
}
item = fromJS(item);
const newState = state.setIn(['sideCart', 'orderItems', item.get('id')], item);
return newState;
}
default:
return state;
}
}
The state should look like this:
sideCart: {
orderItems: {
1: {
id: 'orderItems-1',
name: 'AI Brown\'s BBQ Fillet of Beef with Brown Mushroom Brandy Sauce',
quantity: 10,
price: 12,
subitems: ['0', '1', '2'],
instruction: 'No rosemary for beef',
},
2: {
id: 'orderItems-2',
name: 'AI Brown\'s BBQ Fillet',
quantity: 10,
price: 14,
subitems: ['0', '1', '2'],
instruction: 'No rosemary for beef',
},
},
}
This is how I would enhance it syntactically:
const reduceCart = (state, action) => {
let { item } = action;
const stateIn = state.getIn(['sideCart', 'orderItems', item.id]);
item.quantity = stateIn
? stateIn + 1
: 1;
item = fromJS(item);
return state.setIn(['sideCart', 'orderItems', item.get('id')], item);
};
const globalReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART: return reduceCart(state, action);
default: return state;
}
};
I found the same complexity when using immutable.js to handle deeply nested objects. I have made a lightweight immutable helper: ImmutableAssign that allows you to continue working with plain JS objects, which will simplify your operations.
In the following example, it expects state and action to be plain JS objects, and it will return you a new state as plain JS object:
function globalReducer(state = initialState, action) {
switch (action.type) {
case ADD_TO_CART: return addCart(state, action);
default: return state;
}
}
function addCart(state, action) {
let { item } = action;
return iassign(state,
function(state, context) {
return state.sideCart.orderItems[context.item.id]
},
function(selectedItem) {
item.quantity = selectedItem.quantity ? selectedItem.quantity + 1 : 1;
return item;
},
{ item: item });
}
// The first parameter is a function that return the
// property you need to modify in your state.
// This function must be **pure function**, therefore "item"
// need to be passed in via the context parameter.
//
// The second parameter is a function that modify selected
// part of your state, it doesn't need to be pure, therefore
// you can access "item" directly
//
// The third parameter is the context used in the first
// function (pure function)
Related
Redux, "delete Object" is not working properly in nested Object. I am just trying to delete key "b" in key "0". I am able to just delete the key "0", but when I am trying to delete the nested key "b" in "0", is not working. If you have any idea how to solve it, please, let me know!
Code:
export function removeOrder(key1, key2) {
return {
type: 'REMOVE_ORDER',
payload: {key1: key1, key2: key2},
};
}
var initialState = {
0: {
a: {
title: 'aa',
},
b: {
title: 'bb',
},
c: {
title: 'cc',
},
},
1: {
a: {
title: 'aa',
},
b: {
title: 'bb',
},
c: {
title: 'cc',
},
},
};
function ordersReducer(state = initialState, action) {
switch (action.type) {
case 'REMOVE_ORDER':
const id1 = action.payload.id1;
const id2 = action.payload.id2;
// Working
//delete state[id1];
// Not working
delete state[id1][id2];
return state;
default:
return state;
}
}
removeOrder(0, 'a');
The expected result:
{
0: {
b: {
title: 'bb',
},
c: {
title: 'cc',
},
},
1: {
a: {
title: 'aa',
},
b: {
title: 'bb',
},
c: {
title: 'cc',
},
},
};
You can't delete a field from state like that, as that would mutate the actual state object which is one of the basic no-no's in Redux. From their docs:
https://redux.js.org/usage/troubleshooting#never-mutate-reducer-arguments
It is tempting to modify the state or action passed to you by Redux. Don't do this!
For general patterns see immutable-update-patterns. Specifically for your example you need to create a new state object, spreading the values you do want... An easy way to do this is to use lodash's omitBy to exclude the item with the particular key that you don't want in there anymore.
function ordersReducer(state = initialState, action) {
switch (action.type) {
case 'REMOVE_ORDER':
const id1 = action.payload.id1;
return omitBy(state, (value, key) => key === id1);
default:
return state;
}
}
As others have mentioned in the comments, you can ignore id2 if it's just a sub-key of id1.
If you just want to remove a subkey of id1 then you could use this instead, which is a slight modification on the same approach:
function ordersReducer(state = initialState, action) {
switch (action.type) {
case 'REMOVE_ORDER':
const id1 = action.payload.id1;
const id2 = action.payload.id2;
return {
...state,
[id1]: omitBy(state, (value, key) => key === id2)
};
default:
return state;
}
}
You should not mutate state, you can use spread you don't need to add extra dependencies to your project, omit would be a couple of lines of code:
function omit(state, keys) {
const { [keys[0]]: remove, ...rest } = state;
if (keys.length === 1) {
return rest;
}
return {
...state,
[keys[0]]: omit(remove, keys.slice(1)),
};
}
//test code:
const state = {
0: {
b: {
title: 'bb',
},
c: 'not changed',
},
1: 'not changed',
};
console.log('remove 0', omit(state, ['0']));
console.log('remove 0,b', omit(state, ['0', 'b']));
console.log(
'remove 0,b,title',
omit(state, ['0', 'b', 'title'])
);
As Bravo points out, once you delete the "root" property then all nested properties are also removed.
Other than property deletion you've a state mutation issue since you delete a property and then return the same state object.
You need to return a new copy of state, then you can use a single delete referencing the "full" dynamic path to the second nested key.
case 'REMOVE_ORDER':
const { key1, key2 } = action.payload;
// shallow copy state
const nextState = { ...state };
if (state[key1]) {
// shallow copy the nested state
nextState[key1] = { ...state[key1] }
}
// delete the nested property if it exists
delete nextState[key1]?.[key2];
return nextState;
Hello i just started redux with react native i am trying to make a food delivery app. each food has options like drinks or dessert for example. I would like that if the user adds a basket an items we check if the options chosen are the same to increase the quantity if they are not the same I would like to add another item with the new option to the global state. only I tried a lot of things and none seemed to meet my expectations. the structure of the data that I pass to the global state is in this form.
cartProduct = [
{
id: itemId,
restaurantName: restaurantName,
name: itemName,
quantity: quantity,
price: price
selectedOption: [
optionId: optionId,
itemId: itemId,
name: itemName,
]
}
]
Here is the code I wrote in the reducers with the add to cart action
switch (action.type) {
case 'ADD_TO_CART':
const cartProductIndex = state.cartProduct.findIndex(item => item.id === action.value.id)
if (cartProductIndex == -1) {
nextState = {
...state,
cartProduct: [...state.cartProduct, action.value]
}
} else {
state.cartProduct[cartProductIndex].selectedOption.map(item => {
if (item.item === action.value.selectedOption.item) {
let newArray = [...state.cartProduct]
newArray[cartProductIndex] = {
...newArray[cartProductIndex],
quantity: state.cartProduct[cartProductIndex].quantity + 1,
totalPrice: state.cartProduct[cartProductIndex].totalPrice + action.value.totalPrice
}
nextState = {
...state,
cartProduct: newArray
}
} else {
nextState = {
...state,
cartProduct: [...state.cartProduct, action.value]
}
}
})
}
return nextState || state
I would do something like this.
switch (action.type) {
case 'ADD_TO_CART':
const cartProductIndex = state.cartProduct.findIndex(item => item.id === action.value.id)
if (cartProductIndex == -1) {
return {
...state,
cartProduct: [...state.cartProduct, action.value]
}
}
const newCartProduct = state.cartProduct[cartProductIndex].selectedOption.map(item => {
if (item.item === action.value.selectedOption.item) {
item.quantity++
}
return item
})
return {
...state,
cartProduct: newCartProduct
}
}
You cannot update the state in a map loop, a map returns a new array. You want to use map to create a new array, and then use that value. Once a function is complete you should return, you don't need that else.
I'm working on my first solo ReactJS/Redux project and things were going well until I got to a point where I'm using an object in the Redux store that is always supposed to be a single object. When I copy the object from one part of the store (one element of the sources key) to another (the selectedItems key) that object is being stored as an array of length 1, which isn't the data I'm passing in (it's just a single object). I could live with this and just read out of that store variable as an array and just use element 0 except that when I call another method in the reducer to replace that variable in the store, that method stores the new data as a single object! My preference would be to have everything store a single object but I can't figure out how to do that. Anyway, here's some of the reducer code:
const initialState = {
sources: [
{
id: 1,
mfg: 'GE',
system: 'Smart bed',
model: '01',
name: 'GE smart bed'
},
{
id: 2,
mfg: 'IBM',
system: 'Patient monitor',
model: '03',
name: 'IBM patient monitor'
}
],
error: null,
loading: false,
purchased: false,
selectedItem: {}
};
// This is called when a user selects one of sources in the UI
// the Id of the selected sources object is passed in as action.id
// This method creates an array in state.selectedItem
const alertSourceSelect = ( state, action ) => {
let selectedItem = state.sources.filter(function (item) {
return item.id === action.id;
});
if (!selectedItem) selectedItem = {};
return {...state, selectedItem: selectedItem};
};
// When someone edits the selected source, this takes the field name and new value to
// replace in the selected source object and does so. Those values are stored in
// action.field and action.value . However, when the selected source object is updated
// it is updated as a single object and not as an array.
const selectedSourceEdit = ( state, action ) => {
return {
...state,
selectedItem: updateObject(state.selectedItem[0], { [action.field] : action.value })
};
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.ALERT_SOURCE_SELECT: return alertSourceSelect( state, action );
case actionTypes.ALERT_SELECTED_SOURCE_EDIT: return selectedSourceEdit( state, action );
default: return state;
}
Here is the updateObject method (sorry I left it out):
export const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
}
};
Issue : updateObject is returning object and not array,and you are maintaining selectedItem as an array not object
export const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
}
};
Solution :
Either return array from updateObject :
export const updateObject = (oldObject, updatedProperties) => {
return [{
...oldObject,
...updatedProperties
}]
};
OR make array of returned object
const selectedSourceEdit = ( state, action ) => {
return {
...state,
selectedItem: [updateObject(state.selectedItem[0], { [action.field] : action.value })]
};
};
const state = [
{
list: []
}
];
The list is a list of student objects, for example:
list: [
{ id: 1, name: "Mark", attendance: true },
{ id: 2, name: "John", attendance: false }
]
I have a button that triggers a post request to an API to change attendance to true. Post request returns the student object that was changed as in e.g.:
{ id: 2, name: "John", attendance: true }
This works fine and if no errors, will dispatch ATTENDANCE_SUCCESS.
Now, with this kind of set-up:
export function students(state, action) {
let latestState = state[state.length - 1],
newState = Object.assign({}, latestState);
switch (action.type) {
case "ATTENDANCE_SUCCESS":
if (action.res.errorCode == 0) {
// Need to change redux state 'attendance' value to true for a student with ID returned from the POST request
}
}
Initially, I did:
const studentChanged = newState.list.find(function(student) {
return (
student.id ===
action.res.data.id
);
});
studentChanged.attendance = true;
But it mutates the state in the redux store (although I am not sure how it's exactly happening since I assumed newState is already a copy).
What's the proper way?
The following would update a single item in the array. The critical aspect here is that if the id of the item does not match the id from the action payload, it returns the item unaltered, otherwise it updates the attendance property. Array.prototype.map returns a new array so it would be immutable.
export function students(state, action) {
switch (action.type) {
case "ATTENDANCE_SUCCESS":
if (action.res.errorCode == 0) {
return state.map(student => {
// we want to leave non matching items unaltered
if (student.id !== action.res.data.id) {
return student;
}
return { ...student, attendance: true };
});
}
return state;
default:
return state;
}
}
Here is a StackBlitz to demonstrate the functionality.
Hopefully that helps!
How do i write this inside of an reducer to change the state?
doc = {
id:"zf123ada123ad",
name:"examp",
subdoc:{
name:"subdoc examp",
subsubdoc:[{
id:"zcgsdf123zaar21",
subsubsubdoc:[{
id:"af2317bh123",
value: "heyhey" //this value I want to update
}]
}]
}
}
let's say i have an reducer that looks like this
The action.payload look something like this
{
theInputId1: "someId",
theInputId2: "anotherId",
theInputValue: "someValue"
}
export function updateSubSubSubDoc(state = {}, action){
switch(action.type){
case 'UPDATE_THE_SUBSUBSUB':
return {
state.doc.subdoc.subsubdoc.find(x => x.id ==
theInputId1).subsubsubdoc.find(x => x.id == theInputId2).value = theInputValue // just example code for you to understand where i'm going.
}
default:
return state
}
}
What I want to do it update one subsubsub doc in a state that is current
With ES6, this is one way that you could do that:
const initialState = { doc: { subdoc: { subsubdoc: {} } } };
export function doc(state = initialState, action) {
switch (action.type) {
case 'UPDATE_THE_SUBSUBSUB':
const subsubdocIdx = state.doc.subdoc.
subsubdoc.find(s => s.id == action.theInputId1);
const subsubdoc = state.doc.subdoc.subsubdoc[subsubdocIdx];
const subsubsubdocIdx = state.doc.subdoc.
subsubdoc[subsubdocIdx].
subsubsubdoc.find(s => s.id == action.theInputId2);
const subsubsubdoc = state.doc.subdoc.
subsubdoc[subsubdocIdx].
subsubsubdoc[subsubsubdocIdx];
return {
...state,
doc: {
...state.doc,
subdoc: {
...state.doc.subdoc,
subsubdoc: [
...state.doc.subdoc.subsubdoc.slice(0, subsubdocIdx),
{
...subsubdoc,
subsubsubdoc: [
...subsubdoc.slice(0, subsubsubdocIdx),
{
...subsubsubdoc,
value: action.theInputValue,
},
...subsubdoc.subsubsubdoc.slice(subsubsubdocIdx + 1, subsubdoc.subsubsubdoc.length - 1),
],
},
...state.doc.subdoc.subsubdoc.slice(subsubdocIdx + 1, state.doc.subdoc.subsubdoc.length - 1),
]
}
}
}
default:
return state;
}
}
(I haven’t tested this code.)
This is nested the same level as in your example, but you might consider using something like combineReducers to make this a little easier to manage. This is also presupposing you have other actions that create the document chain along the way, and you know these documents exist.
Here's an example how you might be able to do it with combineReducers:
function doc(state = {}, action) {
}
function subdoc(state = {}, action) {
}
function subsubdoc(state = [], action) {
}
function subsubsubdoc(state = [], action) {
switch (action.type) {
case 'UPDATE_THE_SUBSUBSUB':
const idx = state.find(s => s.id == action.theInputId2);
return [
...state.slice(0, idx),
{
...state[idx],
value: action.theInputValue,
},
...state.slice(idx + 1),
];
default:
return state;
}
}
export default combineReducers({
doc,
subdoc,
subsubdoc,
subsubsubdoc,
});
In this example, you don't need action.theInputId1, but you would need to store some reference in the data from the subsubdoc to the subsubsubdoc so that when you're rendering, you can piece it back together. Same with all of the other layers.