Whenever I click add to cart button, the action is fired but redux state is not been updated (the initial state is not changing but the action is triggered).
const CartScreen = () => {
const { id } = useParams();
const { search } = useLocation();
const [searchParms] = useSearchParams();
const productId = id;
const qty = search ? Number(search.split("=")[1]) : 1;
const dispatch = useDispatch()
useEffect(() => {
if (productId){
dispatch(addToCart(productId, qty))
}
}, [dispatch, productId, qty])
return (
<div>
<h1>Add to CART</h1>
</div>
);
};
export default CartScreen
Cart action
export const addToCart = (id, qty) => async (dispatch, getState) =>{
const {data} = await axios.get(`http://127.0.0.1:8000/api/products/${id}`)
dispatch({
type: CART_ADD_ITEM,
payload:{
product:data._id,
name:data.name,
image:data.image,
countInStock:data.countInStock,
qty
}
})
localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
}
Cart Reducer
export const cartReducer = (state = { cartItems: []}, action) =>{
switch(action.type){
case CART_ADD_ITEM:
const item = action.payload
const existItem = state.cartItems.findIndex(x => x.product === item.product)
if (existItem){
return{
...state,
cartItems: state.cartItems.map(x =>
x.product === existItem.product ? item : x)
}
} else{
return{
...state,
cartItems:[...state.cartItems, item]
}
}
default:
return state
}
}
Redux store
const reducer = combineReducers({
productList: productListReducer,
productDetails: productDetailsReducer,
cart: cartReducer,
})
const initialState = {
cart:{cartItems:cartItemsFromStorage}
};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
From redux dev tools I can see that the action I triggered. The item is getting to cart reducer because when I console.log item in const item=action.payload from the cartReducer, I get the particular item in Browser console, yet the cartItem redux state remains at the initial value, it's not updated
Array.prototype.find()- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
Array.prototype.findIndex()- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
Using Array.prototype.findIndex() will basically look for and return the index of the first found item, and -1 if not found. While Array.prototype.find() returns the first element in the array that matched the criteria provided.
export const cartReducer = (state = { cartItems: [] }, action) => {
switch(action.type){
case CART_ADD_ITEM:
const item = action.payload;
// use Array.prototype.find() instead
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
const existItem = state.cartItems.find(x => x.product === item.product);
if (existItem){
return{
...state,
cartItems: state.cartItems.map(x =>
x.product === existItem.product ? item : x)
};
} else{
return{
...state,
cartItems: [...state.cartItems, item]
};
}
default:
return state;
}
};
The issue is that you are searching for an existing product and returning the found index (array.findIndex) then using the index value as a boolean condition (if (existItem) {...}).
This won't work as you are expecting since all non-zero numbers are truthy, while 0 is falsey. This means if no cart item products match that -1 is returned and the logic will treat this as an existing item. This is compounded later when updating the cart via array.map... if existItem is -1 this means there is no matching product, the new state.cartItems will be a new array, but it will not contain the new item object. In other words it will be just a copy of the previous state.
cartItems starts initially as an empty array, so existItem will always return -1 when first adding an item to the cart.
An additional unintentional bug occurs when a product does exist in the cartItems array and it's the 0th element. existItem will equal 0 and is thus falsey and item will be added to cartItems array as a duplicate.
#Chigbogu is correct regarding the use of array.findIndex and array.find, though I'd recommend using array.some if you are just checking the the cart items array has the item or not. This indicates you are working with a boolean explicitly. Rename existItem to hasItem or similar so the name also indicates a boolean value (this is by convention).
export const cartReducer = (state = { cartItems: []}, action) =>{
switch(action.type) {
case CART_ADD_ITEM: {
const newItem = action.payload;
const hasItem = state.cartItems.some(item => item.product === newItem.product);
if (hasItem) {
return {
...state,
cartItems: state.cartItems.map(item =>
item.product === newItem.product ? newItem : item
)
}
} else {
return {
...state,
cartItems: [...state.cartItems, newItem]
}
}
}
default:
return state;
}
};
Related
I wonder how I can arrange likes for items in a redux or (in my case context) store. I dont know how I can do it without changing the item (that I receive from backend) in the store (to add a possible like to each item)
So far Im doing the following check inside my component:
const SearchResult () => {
const {state: {savedList}} = useContext(SearchContext);
const [isLiked, setIsLiked] = useState(false);
useEffect(() => {
return savedList.find(item => {
if (item._id === searchResult._id) setIsLiked(true);
else setIsiked(false);
})
};
}, []);
...
}
reducer
const likeReducer = (state, action) => {
switch (action.type) {
case 'FETCH_SAVED_ITEMS_SUCCESS':
return {
...state,
isFetching: false,
savedlist: action.payload
}
case 'LIKE_SUCCESS':
return {
...state,
savedlist: action.payload //updated list
}
default:
return state;
}
};
action
const like = dispatch => async (product) => {
dispatch({type: 'LIKE'})
try {
await API.like(product)
dispatch({type: 'LIKE_SUCCESS', payload: product})
} catch(error) {
dispatch({type: 'LIKE_ERROR', payload: error.message})
}
};
Im using react context (not redux).When I put an initial value in my reducer function, every value I pass is fine except 1 which is my notifications array. I pass empty array but initial value is always undefined. I just dont get what is different in 'notifications' vs any other value
const NotificationContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'fetch_notifications':
return {
...state, notifications: action.payload.messages
}
case 'try':
return {
...state, test: action.payload
}
default:
return state;
}
};
export const NotificationsProvider = ({children}) => {
const [state, dispatch] = useReducer(reducer, {notifications: [], test: "test",)
const tryme = () => {
dispatch({type: 'try', payload: "tryingme"})
}
const fetchNotifications = async () => {
const notifications = await API.fetchNotifications();
dispatch({type: 'fetch_notifications', payload: notifications})
};
return (
<NotificationContext.Provider
value={{
state,
fetchNotifications,
tryme
}}
>
{children}
</NotificationContext.Provider>
)
}
I have 3 actions ( add_todo,delete_todo,completed_todo). Add and delete works fine but I should add the deleted items to the completed list in order to render that in a separate component. But whenever I try to use, filter or find to get the deleted items I get a null value.
Reducer code :
const initialState = {
todos: [],
completed: [],
};
const todoSlice = createSlice({
name: "todos",
initialState,
reducers: {
add_todo(state, action) {
state.todos = [...state.todos, action.payload];
},
delete_todo(state, action) {
state.todos = state.todos.filter((todo) => todo.id !== action.payload);
},
completed_todo(state, action) {
console.log(state.todos.find((todo) => todo.id === action.payload));
state.completed = [
...state.completed,
state.todos.filter((todo) => todo.id === action.payload),
];
},
},
});
export const todoActions = todoSlice.actions;
export const selectTodo = (state) => state.todos.todos;
export default todoSlice.reducer;
code where i call or dispatch my actions:
function TodoList() {
const dispatch = useDispatch();
const todos = useSelector(selectTodo);
const handleDelete = (id) => {
dispatch(todoActions.delete_todo(id));
dispatch(todoActions.completed_todo(id));
};
// Some code and a button with handleDelete
}
The actions will be dispatched one after another. After your first action dispatch(todoActions.delete_todo(id));, you will remove the todo from your state .filter((todo) => todo.id !== action.payload).
After that, the second action get's dispatched dispatch(todoActions.completed_todo(id));. But state.todos.find((todo) => todo.id === action.payload) will not find it, since it is already removed.
To fix it, you could swap your dispatch calls. First complete it, then remove it. Problem solved :-)
The problem here is by the time you are dispatching the action to get the completed list your deleted todo's is already gone from the state . Instead of dispatching 2 actions . you can do what are asking for in the delete todo action .
delete_todo(state, action) {
// find the todo to delete
const deletedTodo = state.todos.find((todo) => todo.id === action.payload);
state.completed = [
...state.completed,
deletedTodo,
];
state.todos = state.todos.filter((todo) => todo.id !== action.payload);
},
since your completed todo is nothing but the todo which you are trying to delete . IMHO its logical to do it in the same action which we use to dispatch for deleting a todo .
I'm trying to filter items by their category using useReducer
context.jsx
const initialState = {
categoryName: "all item",
};
const [state, dispatch] = useReducer(reducer, initialState);
const fetchUrl = async () => {
const resp = await fetch(url);
const respData = await resp.json();
const item = respData.item;
const category = respData.category;
const promo = respData.promo;
dispatch({ type: "CATEGORY_ITEM", payload: category });
};
I want to display the category name that matched the data.
reducer.jsx
if (action.type === "FILTER_NAME") {
if (action.payload === "all menu") {
return { ...state, categoryName: "all menu" };
//return { ...state, categoryName: state.categoryName};
} else {
return { ...state, categoryName: action.payload };
}
}
I cant set the categoryName back to the state value because it's been changed when I do else.
Is there a way for me to set a default value in reducer? Because if I use useState the setState won't overwrite the state default value.
Thanks before
I am updating my redux state, and the state doesn't seem to be getting mutated, however the DOM is still not refreshing.
//update filters for events
setFilters = (name) => async () => {
const {onSetActiveEventTypes, authUser} = this.props;
let array = this.props.activeEventTypes
let index = array.indexOf(name);
if (index > -1) {
array.splice(index, 1);
}else {
array.push(name)
}
await Promise.resolve(onSetActiveEventTypes(array));
}
render() {
return <Accordion title="Filters" collapsed>
{
(this.props.eventTypes && this.props.activeEventTypes ?
<EventFilter eventTypes={this.props.eventTypes} activeEventTypes={this.props.activeEventTypes} action={this.setFilters}/>
: '')
}
</Accordion>
}
const mapStateToProps = (state) => ({
eventTypes: state.eventsState.eventTypes,
activeEventTypes: state.eventsState.activeEventTypes
});
const mapDispatchToProps = (dispatch) => ({
onSetEventTypes: (eventTypes) => dispatch({ type: 'EVENT_TYPES_SET',
eventTypes }),
onSetActiveEventTypes: (activeEventTypes) => dispatch({ type:
'ACTIVE_EVENT_TYPES_SET', activeEventTypes })
});
const authCondition = (authUser) => !!authUser;
export default compose(
withAuthorization(authCondition),
connect(mapStateToProps, mapDispatchToProps)
)(DashboardPage);
I have placed my code in my component above, it should be all that is needed to debug. I will put the reducer below
const applySetEventTypes = (state, action) => ({
...state,
eventTypes: action.eventTypes
});
const applySetActiveEventTypes = (state, action) => ({
...state,
activeEventTypes: action.activeEventTypes
});
function eventsReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case 'EVENT_TYPES_SET' : {
return applySetEventTypes(state, action);
}
case 'ACTIVE_EVENT_TYPES_SET' : {
return applySetActiveEventTypes(state, action);
}
default : return state;
}
}
export default eventsReducer;
Above is my reducer, I think I am following the correct patterns for managing redux state and maintaining immutability. What am I missing?
setFilters is a method that the checkboxes use to update active filters compared to all the filters available.
You are definitely mutating state:
const {onSetActiveEventTypes, authUser} = this.props;
let array = this.props.activeEventTypes
let index = array.indexOf(name);
if (index > -1) {
array.splice(index, 1);
}else {
array.push(name)
}
That mutates the existing array you got from the state, and then you are dispatching an action that puts the same array back into the state. So, you are both A) reusing the same array all the time, and B) mutating that array every time.
The approaches described in the Immutable Update Patterns page in the Redux docs apply wherever you are creating new state values, whether you're generating the new state in a reducer based on a couple small values, or before you dispatch the action.
//update filters for events
setFilters = (name) => async () => {
const {onSetActiveEventTypes, authUser} = this.props;
let array = []
this.props.activeEventTypes.map((type) =>{
array.push(type)
})
let index = array.indexOf(name);
if (index > -1) {
array.splice(index, 1);
}else {
array.push(name)
}
//use this once server sending active filters
// await eventTable.oncePostActiveEventTypes(authUser.email, array).then( data
=> {
// Promise.resolve(onSetActiveEventTypes(data));
// })
await Promise.resolve(onSetActiveEventTypes(array));
}