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);
Related
im having an issue with my code, its not populating the state object when state action is being performed. im new with redux
i have this code. so far that having an issue
this is the statement that will called the props.action fetchProjectFamilyList
case 'SubBusinessUnit':
setProductFamilyDetailsObj([])
if (selectedOption.id != 0) {
props.actions.fetchDepartment(selectedOption.id)
props.actions.fetchProjectFamilyList(selectedOption.id)
console.log(props)
}
setDropdownDataInState(resetData, 'Department')
setFormFields({
...formFields,
'OtherNamedInsuredIndustry': {
...formFields.OtherNamedInsuredIndustry,
value: ''
},
'NamedInsuredIndustry': {
...formFields.NamedInsuredIndustry,
value: "",
selectedId: 0
},
[fieldName]: {
...formFields[fieldName],
value: selectedOption.description, selectedId: selectedOption.id
}
});
break;
and this is the code for the commonreducer
export const fetchProjectFamilyList = createAsyncThunk(types.FETCH_PROJECT_FAMILY_LIST,
async (option, {getState, rejectWithValue}) => {
const reduxThunkConfig = {
checkStateData:getState().commonReducer.projectFamilyList && getState().commonReducer.projectFamilyList[option],
rejectWithValue
}
const APIConfig = {
URL: "eapi-referencedata/v1/lists/12?filterBySourceList=" + option + "&filterBySourceListValue=15",
method:"getData",
}
console.log('fetchProjectFamilyList')
return fetchCachedData(reduxThunkConfig, APIConfig);
}
)
im using the builder in my case of course inistailstate is set
const initialState = {
projectFamilyList:{},
}
builder.addCase(fetchProjectFamilyList.fulfilled, (state, action) => {
const subDivision = action.meta.arg;
return {
...state,
projectFamilyList:{
...state.projectFamilyList,
[subDivision]: action.payload},
}})
const commonActions = { ...actions, fetchProjectFamilyList }
export { commonActions, commonReducer}
this is the comment that accept the state as props. but the props productFamilyDetailsObj is empty object
<ProductFamilyComponent
productFamilyDetailsObj={productFamilyDetailsObj}
/>
function ProductFamilyComponent({ productFamilyDetailsObj }) {
return <div className="boxLayout">
<p className="smallHeading">Product Families</p>
{productFamilyDetailsObj.map((text, textIndex) => {
let index = textIndex;
return ( .... and so on
I hope theres someone who could help me resolving this. thank in advance.
I am trying to add bill into the billItems state using redux Toolkit and I don't
want to have multiple same bill with the same Id in the billItems. Each bill is like
{billId: 5, billName: "string"}. I made many attempts but nothing work as I want
First Attempt
export const billSlice = createSlice({
name: "bill",
initialState: {
billItems: [],
},
reducers: (state, action) => {
const arr = [...state.billItems];
if (arr.length === 0) {
state.billItems = [...arr, action.payload];
} else {
const filteredArr = arr.filter(
(item) => item.productId !== action.payload.productId
);
state.billItems = [...filteredArr, action.payload];
}
},
});
With this attempt when I make may dispatch it just change the previous bill and
replace by the new one which is not the needed result
Second attempt
I decide to keep lastBill in the initialSate, take all billItems state with multiple same bill
and go to filter it in the selector using lastBill property.
in the selector like this
export const bSlice = createSlice({
name: "bill",
initialState: {
lastBill: {},
billItems: [],
},
reducers: (state, action) => {
state.billItems = [...state.billItems, action.payload]
});
And my selector is like
const billItems = useSelector(state => {
const arr = state.billItems
const lastBill = state.lastBill
const lastBill_Id = state.lastBill.lastBillId
if(arr.length === 0){
return [...arr, lastBill]
}else{
// I filter to obtain a new array whithout any instance of the lastBill
const filteredArr = arr.filter(item => item.billId !== lastBill_Id)
// I push now the lastBill to be sure that it is unique in the array and return it
return [...filteredArr, lastBill]
}
} )
But with this attempt when i populated the store with the same bill, the store
save it then I have many same bills in the state billItems which is not the needed result
There something that I don't undersatnd. please i need your help
after many research, I have an issue for this. the problem was the reference of objects in Javascripts. I make change in the billSlice like this.
[add_to_billItems_action.fulfilled]: (state, action) => {
let arr = state.billItems;
if (arr.length === 0) {
// I make a copy of arr and modify billItems. At that time arr and billItems have the same payload
state.billItems = [...arr, action.payload];
arr = state.billItems;
} else {
let filteredArr = arr.filter((item) => item.id !== action.payload.id);
state.billItems = [...filteredArr, action.payload];
}
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;
};
I ran into an asynchronous useState problem.
I have a situation where I first need to add an object to the state array in the handler. And then add this state to the localStorage.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
If I delete the entire localStorage first and run the handler. So an empty array is added to my localStorage (the state shows me updated). After the action again, the required object is finally added to my array.
I tried something like this, but still the same problem.
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
localStorage.setItem(storageItemName, JSON.stringify(tempArray));
How do you solve this, please?
/// EDIT
I have something like this
const FavoriteSong = () => {
const song = { id: 1, name: "Lala", length: "3:20" };
const [favoritedSongs, setFavoritedSongs] = useState([]);
const [isFavorited, setIsFavorited] = useState(false);
useEffect(() => {
if (localStorage.getItem("storageItemName")) {
const storageSongs = JSON.parse(
localStorage.getItem("storageItemName") || ""
);
setFavoritedSongs(storageSongs);
const foundSong = storageSongs?.find((song) => song.id === song.id);
foundSong ? setIsFavorited(true) : setIsFavorited(false);
}
}, [song]);
const handleClick = () => {
if (isFavorited) {
const filteredSong = favoritedSongs.filter(
(song) => song.id !== song.id
);
localStorage.setItem("storageItemName", JSON.stringify(filteredSong));
setIsFavorited(false);
} else {
setFavoritedSongs((prev) => [...prev, song]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
setIsFavorited(true);
}
};
return <div onClick={handleClick}>CLICK</div>;
};
export default FavoriteSong;
Just place your localStorage.set logic inside a useEffect to make sure it runs after the state actually changes.
useEffect() => {
localStorage.setItem(...);
}, [favoritedSongs]};
For that you can Use the condition If data in the array then It will set in localStorage otherwise not
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
tempArray.length && localStorage.setItem(storageItemName, JSON.stringify(tempArray));
.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
FavoritedSongs.length(your state name) && localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
Considering this state, I need to select some data from it:
const initialState: PlacesStateT = {
activeTicket: null,
routes: {
departure: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
arrival: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
},
};
so, I came up with two approaches:
first:
const useCoaches = (dir: string) => {
const name = mapDirToRoot(dir);
const carType = useAppSelector((state) => state.places.routes[name].carriageType);
const infoT = useAppSelector((state) => {
return state.places.activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
});
const coaches = infoT.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carType;
});
return coaches;
};
and second:
const handlerActiveCoaches = (name: string) => (state: RootState) => {
const { carriageType } = state.places.routes[name];
const { activeTicket } = state.places;
const trainInfo = activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
return trainInfo.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carriageType;
});
};
const useActiveInfo = (dir: string) => {
const routeName = mapDirToRoot(dir);
const selectActiveCoaches = handlerActiveCoaches(routeName);
const coaches = useAppSelector(selectActiveCoaches);
return coaches;
};
Eventually, if the first one works ok then the second one gives a lot of useless re-renders in component. I suspect that there are problems with selectActiveCoaches closure, maybe react considers that this selector is different on every re-render but I am wrong maybe. Could you explain how does it work?
selectActiveCoaches finishes with return seatsTrainInfo.filter(). This always returns a new array reference, and useSelector will force your component to re-render whenever your selector returns a different reference than last time. So, you are forcing your component to re-render after every dispatched action:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
One option here would be to rewrite this as a memoized selector with Reselect:
https://redux.js.org/usage/deriving-data-selectors