Can someone please explain me this code from ReactJS? - reactjs

can anyone please explain me this code? I am not able to understand as in what's happening here.
const cartReducer = (state, action) => {
if (action.type === "ADD") {
const updatedTotalAmount =
state.totalAmount + action.item.price * action.item.amount;
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.item.id
);
const existingCartItem = state.items[existingCartItemIndex];
let updatedItems;
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
updatedItems = [...state.items];
updatedItems[existingCartItemIndex] = updatedItem;
} else {
updatedItems = state.items.concat(action.item);
}
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
return defaultCartState;
};

That is a redux reducer. Please read this tutorial to get familiar with the concepts of it:
https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers

Reducers were popularized by Redux but are not a concept inherent to Redux in the sense that you can write a reducer without any import from Redux. A reducer is a concept for a particular kind of function i.e.:
a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState. "Reducer" functions get their name because they're similar to the kind of callback function you pass to the Array.reduce() method.
Source: Redux docs
React now comes with a useReducer hook built-in. See Hooks API Reference.

I have added some comments to your code I hope this makes the code a bit more understandable.
const cartReducer = (state, action) => {
// Adding an Item to Cart
if (action.type === "ADD") {
// Calculated Cart Total: existing Total + (new Item Price * new item Quantity)
const updatedTotalAmount = state.totalAmount + action.item.price * action.item.amount;
/*
* Finding Items Index in the Cart Array using the Item ID.
* Index will be Returned only if Item with same od already exist otherwise -1
*/
const existingCartItemIndex = state.items.findIndex((item) => item.id === action.item.id);
/*
* Getting the CartItem Based on the Index.
* If the value is -1 i.e., item already doesn't exist, then this code will return undefined
*/
const existingCartItem = state.items[existingCartItemIndex];
let updatedItems;
// existingCartItem will have an Object(which evaluates to true) only if Item already existed in Cart
if (existingCartItem) {
// Creating updatedItem by spreading the existingItems data + updating amount/Quantity to: existing Quantity + new Quantity
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
// Making a Copy of Items Array & Replacing Existing Item with new/updated entry
updatedItems = [...state.items];
updatedItems[existingCartItemIndex] = updatedItem;
} else {
// If the Item doesn't already exist in Cart then we Just add that New Item to the Cart
updatedItems = state.items.concat(action.item);
}
// Return the State with Updated Items List & total Amount
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
// Default State Return
return defaultCartState;
};

Related

update state in useReducer()

first of all I have to mention that i'm a bit new to react but I've spent couple of hours on a problem with useReducer and couldn't understand it . here is my code:
const defaultCartState = {
items: [],
totalAmount: 0,
};
//action = {type:"ADD" , item:item}
const cartReducer = (state, action) => {
if (action.type === "ADD") {
const updatedTotalAmount = parseFloat(
state.totalAmount + action.item.price * action.item.amount
).toFixed(2);
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.item.id
);
const existingCartItem = state.items[existingCartItemIndex];
let updatedItems;
if (existingCartItem) {
const updatedItem = state.items[existingCartItemIndex];
updatedItem.amount =
state.items[existingCartItemIndex].amount + action.item.amount;
const updatedItems = [...state.items];
updatedItems[existingCartItemIndex] = updatedItem;
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
} else {
const updatedItems = state.items.concat(action.item);
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
}
return defaultCartState;
};
its simply my reducer function state include an array named items which each element of this array is an object with a amount value. and here is my useReducer initialization:
const [cartState, dispatchCartAction] = useReducer(cartReducer,defaultCartState);
my problem is with these two lines of code :
if (existingCartItem) {
const updatedItem = state.items[existingCartItemIndex];
updatedItem.amount =
state.items[existingCartItemIndex].amount + action.item.amount;
if I swap these two lines of code with these everything works fine .
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
I want to know what is the problem ? why my approach doesn't work? what is the difference between defining an item like my solution and the real solution ?
thank you in advance
As reducers are pure functions you cannot mutate state directly. That is why the second method is working. You need to copy the previous state and then and create a new object every time.
I think this may help more (if I understood the question correctly);

Redux business logic best practice

Im building a shopping cart with React and Redux and failing to understand the best practice flow.
My Cart ACTIONS:
export const addToCart = (product) => (dispatch, getState) => {
let { products: cartProducts } = getState().cart;
let { newCartProducts, totalPrice } = handleAddToCart(cartProducts, product);
dispatch(
add({
products: newCartProducts,
total: totalPrice,
})
);
};
Mock-Server handlers: (All the logic of updating the product is going here => my main question is if this is makes any sense.
export function handleAddToCart(cartProducts, currentProduct) {
let idx = cartProducts.findIndex((p) => p.id === currentProduct.id);
let productInCart = cartProducts[idx];
if (productInCart) {
let updatedProduct = {
...currentProduct,
quantity: productInCart.quantity + 1,
price:
productInCart.price +
applySale({
...currentProduct,
quantity: productInCart.quantity + 1,
currentTotal: productInCart.price,
}),
};
cartProducts.splice(idx, 1, updatedProduct);
} else cartProducts.push({ ...currentProduct, quantity: 1 });
let totalPrice = cartProducts.reduce((acc, val) => (acc += val.price), 0);
return { newCartProducts: cartProducts, totalPrice };
}
Cart reducer:
};
export default (state = DEFAULT_STATE, action) => {
switch (action.type) {
case "ADD_TO_CART":
return {
products: [...action.payload.products],
total: action.payload.total,
};
default:
return DEFAULT_STATE;
}
};
As you see from the code, I kept action and reducer logic to minimum and let the handler manipulate data. only after the data is manipulated, I insert it to the state.
After giving it thought, the reducer ADD_TO_CART is only symbolic because it gets an array and not an item so it can be actually a multi purpose reducer which is i think not so good.
Would be great to hear more opinions.
We specifically recommend putting as much logic as possible into reducers, and treating actions as "events" that describe "what happened" with the minimal amount of data inside.
Also, note that you should be using our official Redux Toolkit package, which will drastically simplify your Redux logic.

How to avoid firing multiple redux actions with real time firestore listeners

Introduction
A Little Warning: I do use Redux Toolkit
I have bunch of lists, one of which should be active. And depending on some context, active list should be different. For example I have 3 lists (A, B, C) and let's look at following patterns:
List B is active and I decided to create a new list. After creating list D, list D should be active:
List D - active
List C
List B
List A
List B is active and I decided to change the page. When I come back, List B should be active as it was before changing the page.
The problem
As I initiate the setListsAction from the beginning, it always listens to the firestore and gets invoked every time I manipulate with the store (add, remove, update) and then pass all the data to the reducer. For this reason, I can't control which action was actually performed. For this case in my setListsReducer I check if there's already an active list, if so, I don't change it (covering my second pattern in the examples section). However, with such logic I can't set newly created list as active, because there'll be always an active list that's why in my createListAction I pas a newly created list to the payload and in createListReducer I set payload as the active list. However, the caveat of this approach is that both setListsAction and createListAction gets triggered, so redux state gets updated two times in a row, making my components rerender unnecessary. The cycle looks like that:
in createListAction I add list to the firestore
firestore was updated, so setListsAction gets triggered
createListAction dispatches fulfilled action.
My Code
Actions
setListsAction
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = await db.collection('lists');
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
const lists = querySnapshot.docs.map((doc) => {
const list = { ...doc.data(), id: doc.id };
return list;
});
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
});
};
};
createListAction
export const createListActionAsync = (list) => {
return async (dispatch: Dispatch<PayloadAction<any>>) => {
dispatch(listsLoadingActions.pending());
const docList = await db.collection('lists').add(list);
const fbList = { ...list, id: docList.id };
dispatch(listsLoadingActions.fulfilled(fbList));
};
};
Reducers
setListsReducer
builder.addCase(fetchLoadingActions.fulfilled, (state, { payload }) => {
state.lists = payload.lists;
const activeList = state.activeList
? payload.lists.find((l) => l.id === state.activeList.id)
: payload.lists[0];
state.activeList = activeList;
});
createListReducer
builder.addCase(listsLoadingActions.fulfilled, (state, { payload }) => {
state.activeList = payload;
});
Sum
So I would like you to propose a better way to handle my problem. I tried to solve it, using change type on docChanges but when I init setListsAction, all docs' changes are type of added and workarounds may damage further implementations of the app. Probably, I need to give up real time database and use get method instead.
If you eliminate the createListReducer and listLoadingActions, you should be able to do everything from inside the ListsAction hook. Using await db.collection('lists').add(list) should refire the listener on the lists collection once it's been added to the database successfully.
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = db.collection('lists'); // no need to await?
let firstLoad = true; // used to determine whether to use docs or docsChanges
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
if (firstLoad) {
const lists = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
firstLoad = false;
// Get and set initial active list?
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
} else {
// optionally fire dispatch(fetchLoadingActions.pending()) again?
const listsCopy = [...state.lists]; // copy the existing list to mutate it
let activeList = state.activeList; // store the current activeList
querySnapshot.docChanges().map((change) => {
if (change.type === "added") {
const thisList = { ...change.doc.data(), id: change.doc.id };
listsCopy.splice(change.newIndex, 0, thisList);
activeList = thisList;
} else if (change.type === "modified") {
listsCopy.splice(change.oldIndex, 1);
listsCopy.splice(change.newIndex, 0, { ...change.doc.data(), id: change.doc.id });
} else if (change.type === "removed") {
listsCopy.splice(change.oldIndex, 1);
if (activeList.id === change.doc.id) {
// the current active list was removed!
activeList = undefined;
}
}
});
dispatch(
fetchLoadingActions.fulfilled({
lists: listsCopy,
activeList: activeList || listsCopy[0] // use activeList or fallback to first list in listsCopy, could still be undefined if listsCopy is empty!
})
);
}
});
return unsubscribe;
};
};
Regarding the active list history, you could either use the URL ?list=some-id to store the selected list with the History API or you could store an array called activeListHistory in your state variable where you push() and pop() to it as necessary (make sure to handle cases where the old list no longer exists and where there are no entries in the array).

Redux change a reducer of type object

So I have the following reducer
const objectType = (state = {type: 0, image:defaultImage, moreOptions: {tap: 0, griff: 0} },
action) => {....
case 'CHANG_OPTIONS':
return state = {...state, moreOptions: {tap:action.tap, griff: action.griff}}
This is the action, so I get a dynamic category and assign the id of the product.
export const changeOptions = (category, id) => {
return {
type: 'CHANG_OPTIONS',
[category]: id,
}
}
An example of dispatch would be
dispatch(changeOptions('tap', 0))
Now whenever I click on a tap or a griff, my object remove the other category from the list.
Here is a screenshot from the Redux Debugger tool
I'm sure that the problem is in my reducer:
moreOptions: {tap:action.tap, griff: action.griff} Is there a way I can spread the object and update only the one that was changed?
It's because you're overwritting both tap and griff value regardless of their input value. Try below.
const newOptions = {};
if (action.tap) {
newOptions.tap = action.tap;
}
if (action.griff) {
newOptions.griff = action.griff;
}
return (state = {
...state,
moreOptions: {
...state.moreOptions,
...newOptions
}
});

Why reducer with array.sort doesn't trigger re-render?

I've got a problem with re-rendering table in React.
So at first is how it looks like
When I fetch data from SWAPI (I'm doing it in action) and passing it into reducer, it works fine and table component re-renders without any trouble.
But when I'm trying to sort it via SORT_RESULTS reducer, it just doesn't do anything with table itself. It actually modifies state as FETCH_DATA reducer do, but doesn't do anything, however I can log sorted information and make sure it was sorted as intended.
Thanks in advance for your help!
// actions.ts
export const onDataFetched = (currentPage: number = 1) => (
async (dispatch:any) => {
let data: {} = await fetch(`http://swapi.dev/api/people/?page=${currentPage}`)
.then(response => response.json())
const dataToReturn = {
type: 'FETCH_DATA',
payload: data
}
dispatch(dataToReturn)
}
)
// reducers.ts
case FETCH_DATA:
// returns new state object
return {
...state,
swapi_data: {
...action.payload
}
}
// actions.ts
export const onSortResults = (payload: string) => (
(dispatch:any) => {
dispatch({type: 'SORT_RESULTS', payload})
}
)
// reducers.ts
case SORT_RESULTS:
let results = state.swapi_data.results;
// title is heading of a table column (e.g. 'name', 'weight', etc)
const title = action.payload;
// sorts array with persons (objects)
results.sort((a: any, b: any) => {
return (a[title] > b[title]) ? 1 : ((b[title] > a[title]) ? -1 : 0)
})
// as in example with FETCH_DATA reducers it's doing the same job - returns new state object
return {
...state,
swapi_data: {
...state.swapi_data,
results: results,
},
}
Array Sort Mutates
If you mutate there is no change detected since the reference is the same. But sort also returns itself so you can add this code. The spread operator makes a copy and there for changes the reference meaning that your changes will be detected.
const sortedResults = results.sort((a: any, b: any) => {
return (a[title] > b[title]) ? 1 : ((b[title] > a[title]) ? -1 : 0)
})
return {
...state,
swapi_data: {
...state.swapi_data,
results: [...sortedResults],
},
}

Resources