React-Redux How to improve code in Reducers file - reactjs

I am currently working on an upvote/downvote feature and have shown state changes of how it on my reducers. I feel like the code in 'case types.ADD_VOTE' in my reducers file could be refactored to be cleaner.
I also included the container file as well to better understand what I am trying to achieve in the app.
Reducers
import * as types from '../constants/actionTypes.js';
/*
#giftList: List of objects
#lastGiftId:
*/
const initialState = {
giftList: [],
lastGiftId: 10000,
totalVotes: 0,
alreadyVoted: false,
newMessage: ''
};
const giftReducer = (state=initialState, action) => {
// let giftList;
// let setMessage;
switch(action.type) {
case types.ADD_GIFT:
let stateCopy = {...state};
stateCopy.lastMarketId += 1;
// create the new gift object structure.
const giftStructure = {
// lastGiftId: stateCopy.lastGiftId,
newMessage: stateCopy.newMessage,
totalVotes: 0
};
return {
...state,
lastMarketId: stateCopy.lastMarketId,
giftList: [...state.giftList, giftStructure],
newMessage: ''
}
case types.SET_MESSAGE:
return {
...state,
newMessage: action.payload,
}
case types.ADD_VOTE:
let stateCopy2 = {...state};
console.log("Already Voted Before: ", stateCopy2.alreadyVoted);
if(stateCopy2.alreadyVoted) {
stateCopy2.totalVotes -= 1;
stateCopy2.alreadyVoted = false;
} else {
stateCopy2.totalVotes += 1;
stateCopy2.alreadyVoted = true;
}
console.log("Already Voted after: ", stateCopy2.alreadyVoted);
return {
...state,
totalVotes: stateCopy2.totalVotes,
alreadyVoted: stateCopy2.alreadyVoted
}
default:
return state;
}
};
export default giftReducer;
List Container
const mapDispatchToProps = dispatch => ({
updateGiftMessage: (e) => {
console.log(e.target.value);
dispatch(actions.setMessage(e.target.value));
},
addGift: (e) => {
e.preventDefault();
console.log("actions: ", actions.addGift);
dispatch(actions.addGift());
},
addVote: (e) => {
e.preventDefault();
//console.log("event: ", e.target.getAttribute('mktid'));
dispatch(actions.addVote(e.target.getAttribute('gift-id')));
}
// }
});
class ListContainer extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div className="All-Lists">
<h1>LIST CONTAINER!</h1>
<AllGiftsDisplay giftList = {this.props.giftList} addGift={this.props.addGift} setNewMessage={this.props.setNewMessage} totalVotes = {this.props.totalVotes} lastGiftId = {this.props.lastGiftId} addVote = {this.props.addVote} lastGiftId = {this.props.lastGiftId}/>
<GiftCreator setNewMessage={this.props.setNewMessage} updateGiftMessage={this.props.updateGiftMessage} addGift={this.props.addGift}/>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ListContainer);

If you don't need to log any intermediate state, or pre/post state:
case types.ADD_VOTE:
return {
...state,
totalVotes: state.totalVotes + (state.alreadyVoted ? -1 : 1),
alreadyVoted: !state.alreadyVoted,
};
Increment/Decrement total votes on value of already voted, and toggle the already voted value.
Using object destructuring, reduces code a little bit
case types.ADD_VOTE:
const { alreadyVoted, totalVotes } = state;
return {
...state,
totalVotes: totalVotes + (alreadyVoted ? -1 : 1),
alreadyVoted: !alreadyVoted,
};

Related

How to access the updated value in my store immediately after updating it?

I have a React application that is currently using Redux for state management.
What I am trying to achieve: Click a Buy Now button - dispatch a action that makes a request to the server to add the item (increment the cart item count based on server response), check the state to see if the cart item count is greater than 0 & do something if it is.
For some reason, I have to click the button twice in order for the cartItemCount to reflect 1?
My current implementation looks like the below (I have tried to pull out all the unrelated code due to the file being quite large):
CourseSpecificScreen.tsx
const mapStateToProps = (state: RootState) => {
return {
courseSpecificReducer: state.courseSpecificReducer,
authState: state.authReducer,
currencyState: state.currencyReducer,
cartReducer: state.cartReducer,
courseCategoriesState: state.courseCategoriesReducer,
};
};
const mapDispatchTopProps = (dispatch: Dispatch<AnyAction>) => {
return bindActionCreators(ActionCreators, dispatch);
};
const connector = connect(mapStateToProps, mapDispatchTopProps);
type CourseSpecificScreenNavigationProp = CompositeNavigationProp<
StackNavigationProp<ExploreRouteStackParamList, "CourseSpecificScreen">,
CompositeNavigationProp<
StackNavigationProp<AppRouteHeaderParamList>,
StackNavigationProp<AuthRouteStackParamList>
>
>;
type CourseSpecificScreenRouteProp = RouteProp<
ExploreRouteStackParamList,
"CourseSpecificScreen"
>;
type Props = PropsFromRedux & {
navigation: CourseSpecificScreenNavigationProp;
route: CourseSpecificScreenRouteProp;
};
type State = {
cartItemCount: number;
};
class CourseSpecificScreen extends Component<Props, State> {
pruchaseItem = async () => {
const {
courseSpecificReducer,
clearCartAndAddItem,
navigation,
cartReducer,
getCartItemCount,
} = this.props;
const paymentMethod = paymentMethodForDevice();
await clearCartAndAddItem(
paymentMethod,
courseSpecificReducer.productData.code as string,
1,
navigation
)
if(cartReducer.cartItemCount > 0) {
// do some stuff
}
};
render() {
return (
<Button
btnStyle={[this.getStyles().smallButtonBuyCourse]}
labelStyle={[this.getStyles().buttonStickyLabelStyle]}
label={translate(
productData.isBundle && productData.isBundle === true
? "CategorySpecificScreen_buyThisBundle"
: "CategorySpecificScreen_buyThisCourse",
)}
onPress={this.purchaseItem}
disabled={false}
/>
)
};
CourseSpecificScreen.contextType = LocalizationContext;
export default connector(CourseSpecificScreen);
ThunkActions.ts
export const clearCartAndAddItem = (
paymentMethod: string,
productCode: string,
quantity: number,
navigation: any,
): AppThunk => {
return async (dispatch) => {
dispatch(cartActions.updateCartLoadingStatus(true));
const response = await cartServices.clearCart();
const {httpStatusCode} = response as APIResponse;
switch (httpStatusCode) {
case httpStatusCodes.SUCCESS_OK:
case httpStatusCodes.SUCCESS_CREATED:
case httpStatusCodes.SUCCESS_NO_CONTENT:
dispatch(cartActions.updateCartLoadingStatus(false));
dispatch(cartActions.updateCartItemCount(0))
globalConfig.setCartItemCount(0);
dispatch(addItemToCart(paymentMethod, productCode, quantity, navigation));
break;
case httpStatusCodes.CLIENT_ERROR_UNAUTHORIZED:
case httpStatusCodes.SERVER_ERROR_INTERNAL_SERVER_ERROR:
dispatch(cartActions.updateCartLoadingStatus(false));
let alertMessage = "Error, please try again later.";
if (response?.message) alertMessage = response?.message;
Alert.alert("Alert", alertMessage, [
{
text: "Ok",
},
]);
break;
default: {
dispatch(cartActions.updateCartLoadingStatus(false));
}
}
};
};
export const addItemToCart = (
paymentMethod: string,
productCode: string,
quantity: number,
navigation: any,
): AppThunk => {
return async (dispatch) => {
dispatch(cartActions.updateCartLoadingStatus(true));
const response = await cartServices.addItemToCart(productCode, quantity, paymentMethod);
const {httpStatusCode, data, error, message} = response as APIResponse;
console.log('add_item_to_cart_response:', response);
switch (httpStatusCode) {
case httpStatusCodes.SUCCESS_OK:
case httpStatusCodes.SUCCESS_CREATED:
dispatch(cartActions.updateCartLoadingStatus(false));
dispatch(cartActions.updateCartItemCount(quantity));
globalConfig.setCartItemCount(quantity);
break;
case httpStatusCodes.CLIENT_ERROR_UNAUTHORIZED:
dispatch(cartActions.updateCartLoadingStatus(false));
break;
case httpStatusCodes.SERVER_ERROR_INTERNAL_SERVER_ERROR:
case httpStatusCodes.CLIENT_ERROR_BAD_REQUEST:
dispatch(cartActions.updateCartLoadingStatus(false));
Alert.alert("Alert", (message)? message : "Error, it looks like you already have access to this course.", [
{
text: "Ok",
},
]);
break;
default: {
dispatch(cartActions.updateCartLoadingStatus(false));
}
}
};
};
Reducers.ts
const initialState: CartInitialState = {
isLoading: true,
cartToken: "",
responseStatus: apiResponseStatuses.IDLE,
cartItemCount: 0,
isMessageVisible: false,
message: "",
};
export default function cartReducer(
state = initialState,
action: CartActionTypes,
): CartInitialState {
switch (action.type) {
case UPDATE_LOADING_STATUS:
return {
...state,
isLoading: action.isLoading,
};
case UPDATE_CART_TOKEN:
return {
...state,
cartToken: action.cartToken,
};
case UPDATE_RESPONSE_STATUS:
return {
...state,
responseStatus: action.responseStatus,
};
case UPDATE_CART_ITEM_COUNT_TOKEN:
return {
...state,
cartItemCount: action.cartItemCount,
};
case CLEAR_DATA_ON_LOGOUT:
return {
...state,
isLoading: true,
cartToken: "",
responseStatus: apiResponseStatuses.IDLE,
cartItemCount: 0,
isMessageVisible: false,
message: "",
};
default: {
return state;
}
}
}
In the pruchaseItem() function of CourseSpecificScreen.tsx, I would like to dispatch a action that adds the item to the cart and immediately afterwards check if the cartItemCount has been updated & if it has, do something... This functionality works as expected, but only after clicking the Buy Now button twice.
I have ruled out the possibility of the issue being the API request failing the first time.
I have been stuck on this issue for several days now so any help or advice would be greatly appreciated. Let me know if I need to include more information
In my case, I was storing a reference of the old cartReducer state before it was being updated.
I got this working by updating my purchaseItem() function to look like the below:
pruchaseItem = async () => {
const {
courseSpecificReducer,
clearCartAndAddItem,
navigation
} = this.props;
const paymentMethod = paymentMethodForDevice();
await clearCartAndAddItem(
paymentMethod,
courseSpecificReducer.productData.code as string,
1,
navigation
)
const { cartReducer } = this.props;
if(cartReducer.cartItemCount > 0) {
// do some stuff
}
};

++ increments value by 2 instead of 1

I am using the following reducer to add products, delete them, and add them to a cart. I would like the 'ADD_TO_CART' method when called to add an item to the cart, but if an item is already in the cart, I would like to increase it by 1. However, the method is increasing cart items by 2 instead of 1
export default (state, action) => {
switch (action.type) {
case 'ADD_PRODUCT':
return {
...state,
products: [action.payload, ...state.products]
}
case 'DELETE_PRODUCT':
return {
...state,
products: state.products.filter(product => product.id !== action.payload)
}
case 'ADD_TO_CART': if (state.cart.some(cartItem => cartItem.id === action.payload.id)) {
const updatedItemIndex = state.cart.findIndex(cartItem => cartItem.id === action.payload.id)
let cartItems = [...state.cart]
let itemToUpdate = cartItems[updatedItemIndex]
itemToUpdate.quantity++
return { ...state, cart: cartItems }
} else {
const newItem = { ...action.payload, quantity: 1 }
return { ...state, cart: [...state.cart, newItem] }
}
default:
return state
}
}
This is the code dispatching the action
function addToCart(product) {
dispatch({
type: 'ADD_TO_CART',
payload: product
})
}
I am calling the action from the following component
import React, { useContext } from 'react'
import { ProductContext } from './ProductContext'
export const ProductCard = ({ product }) => {
const { deleteProduct, addToCart } = useContext(ProductContext)
const handleDelete = () => {
deleteProduct(product.id)
}
const handleCart = () => {
addToCart(product)
}
return (
<div className='product__card'>
<h3>{product.productName}</h3>
<p>{product.price}</p>
<button className='button'>View Product</button>
<button onClick={handleCart} className='button'>Add to Cart</button>
<button onClick={handleDelete} className='button'>Delete</button>
</div>
)
}
Try changing the quantity from a number to an empty quote.
Old: const newItem = { ...action.payload, quantity: 1 }
New: const newItem = { ...action.payload, quantity: '' }
You have a mistake. You MUTATE your state.
let cartItems = [...state.cart] // <-- reinitialize only the array,
// the array items remains with the same reference!!!!
let itemToUpdate = cartItems[updatedItemIndex]
itemToUpdate.quantity++ // <--- this will mutates your state!!!
return { ...state, cart: cartItems }
Fix: create a new item, and copy the data with the spread operator, and delete the old one.
I have ran into the same problem with a very similar situation, and none of the current answers helped me. Therefore I can only recommend
itemToUpdate.quantity++
Be changed into
itemToUpdate.quantity += 0.5;
I know that this is not the correct way to solve this problem, but the functionality works, and you can change it later when you will find the solution

React redux map reducer

I have looked everywhere but nothing is working. I am trying to increment/decrement position based on which one they select on screen.
I am using useSelector to try and get a slice of the code.
const leagueSettings = useSelector((state) => state.drafts.rosterSettings);
But I cannot get the mapping to work. The state never updates. I can see that I am sending in an updated count for the position and the correct position.
My action.js
export const ADD_ROSTER_SPOT = "ADD_ROSTER_SPOT";
export const REMOVE_ROSTER_SPOT = "REMOVE_ROSTER_SPOT";
export const addRosterSpot = (position, count) => {
return {
type: ADD_ROSTER_SPOT,
position: position,
amount: count
};
};
export const removeRosterSpot = (position, count) => {
return {
type: REMOVE_ROSTER_SPOT,
position: position,
amount: count
};
};
my reducer.js
import { ADD_ROSTER_SPOT, REMOVE_ROSTER_SPOT } from "../actions/draft";
let initialState = {
leagueSettings: [], // list of league settings,
rosterSettings: [
{ Position: "QB", amount: 1 },
{ Position: "WR", amount: 1 },
{ Position: "RB", amount: 1 },
{ Position: "TE", amount: 1 },
],
drafted: [], // list of players drafted by others
};
export default (state = initialState, action) => {
switch (action.type) {
case ADD_ROSTER_SPOT:
return state.rosterSettings.map(pos => {
if (pos.Position === action.position) {
return {...pos, amount: pos.amount + 1}
}
console.log(state)
return pos;
});
case REMOVE_ROSTER_SPOT:
return state.rosterSettings.map((pos) => {
if (pos.Position === action.position) {
return { ...pos, amount: pos.amount - 1 };
}
return pos;
});
default: return state;
}
};
You need to change the code inside the reducer because it is not updating the state correctly
try using this in the "ADD_ROSTER_SPOT" case in reducer
let filteredState = state.rosterSettings.filter((eachPos) => {
if (eachPos.Position === action.Position) {
eachPos.amount = eachPos.amount + 1;
return eachPos;
}
return eachPos;
});
let newState = Object.assign({}, state, { rosterSettings: filteredState });
return({ ...newState });
Please check where the drafts reducer is defined, and what is your code in rootReducer?

Removing Item from array in reducer not working

I am trying to remove single item from cart in reducer but not it does not seems to work. itemsInCart is Updated in ADD_TO_CART but not in REMOVE_FROM_CART.
Can anyone suggest edit to my code....
I tried passing mutable/immutable params to manageItemCount()
function manageItemCount(allItems, newItem){
let itemIndex = [];
if(allItems.length > 0) {
allItems.forEach((elem, i) => {
if (elem.product.id == newItem.product.id) {
itemIndex.push(i);
};
});
if(itemIndex.length){
allItems.splice(itemIndex.length-1, 1);
}
}
return allItems;
}
let alreadyRemovedFromCart = false;
const cartReducer = (state = {
itemsInCart: []
}, action) => {
switch (action.type) {
case 'ADD_TO_CART':
state = {
...state,
itemsInCart: [...state.itemsInCart, action.payload]
};
break;
case 'REMOVE_FROM_CART':
state = {
...state,
itemsInCart: manageItemCount(...state.itemsInCart, action.payload)
};
break;
}
return state;
}
export default cartReducer;
manageItemCount accepts two parameters but you are spreading all the itemsInCart array. So it should be:
case 'REMOVE_FROM_CART':
state = {
...state,
itemsInCart: manageItemCount(state.itemsInCart, action.payload)
};
break
Also manageItemCount seems like it is doing just .filter on itemsInCart.

Delete an item from an array in Redux

I'm learning redux and I was wondering how to delete one item from the state. I have this initial state:
export const getInitialState = () => {
let state = {
isLogged: false,
organizations: [],
userData: {},
activeIndex: -1,
currentRetrospective: {},
hasFetched: false
}
This is how the data lives inside organizations
case `${actions.ACTION_GET_USER_ORGS}_FULFILLED`: {
let activeIndex = 0
if (state.activeIndex !== -1) {
activeIndex = state.activeIndex
} else if (action.payload.data.length === 0) {
activeIndex = -1
}
return { ...state, activeIndex, organizations: action.payload.data, hasFetched: true }
}
Now, what I need to do is to delete one item from the retrospectives array in an organization. I tried this but it doesn't work. Is there a better way to do it?
export default (state = getInitialState(), action) => {
switch (action.type) {
case `${actions.ACTION_DELETE_RETROSPECTIVE}_FULFILLED`: {
const { organizations, activeIndex } = state
const newOrganizations = JSON.parse(JSON.stringify(organizations))
const activeOrganization = newOrganizations[activeIndex]
activeOrganization.retrospectives = activeOrganization.retrospectives
.filter((retro) => retro.id != action.retroId )
return { ...state, organizations: newOrganizations }
}
Thank you!
you can filter the organization array like this:
export default (state = getInitialState(), action) => {
switch (action.type) {
case `${actions.ACTION_DELETE_RETROSPECTIVE}_FULFILLED`: {
return {
...state,
organizations: state.organization.filter(retro =>
retro.id !== action.retroId }
}

Resources