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 .
Related
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;
}
};
I have 2 questions on managing the state for my app.
Part 1: I can't update and set the entire state with my reducer, when I fetch the data from the server, but can change a part of that state:
state.main_meals = action.payload - works
state = action.payload - doesn't work
Part 2: I have heard that you shouldn't be using pure "setter" functions in redux, but how else am I supposed to set my state?
here is my reducer:
setDiaryState: (state, action) => {
state = action.payload;
},
and my component:
const diary = useAppSelector((state) => state.diary);
const dispatch = useAppDispatch();
const [user, loading, error] = useAuthState(auth);
useEffect(() => {
{
user &&
db
.collection("users")
.doc(`${auth.currentUser?.uid}`)
.collection("diaryState")
.doc("currentState")
.onSnapshot((doc) => {
const data = doc.data();
dispatch(setDiaryState(data));
});
}
}, [user]);
Your reducer must always return a immutable state here,
state = action.payload;
You are clearly mutating it which won't trigger a re-render,this can be fixed by simply add a return statement.
This would replace your entire state by action.payload.
setDiaryState: (state, action) => {
return {...action.payload};
},
if you wish to modify a subset of your state you can use:
setDiaryState: (state, action) => {
return {...state, main_meals:action.payload};
},
This way you ensure that your state is always immutable.
I have two reducers, one with the name of Month and the other with Loading
export const Month = (state = '', action) =>
action.type === 'selectedMonth' ? (state = action.payload) : '';
Second one,
export const isLoading = (state = false, action) => {
switch (action.type) {
case 'setLoading':
return !state;
case 'removeLoading':
return false;
default:
return state;
}
};
I'm using dispatch inside the useEffect multiple times for the different actions. At first call i-e dispatch(actions.setMonths()) works fine but when I recall the dispatch for the different action i-e dispatch(actions.setLoading()) the store gets updated but the state of the month is set to the initial state.
dispatcher calls,
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
dispatch(actions.setLoading());
const res = await getData('users');
dispatch(actions.removeLoading());
};
//any code
dispatch(actions.selectedMonth("argument"));
fetchData();
}, [months, dispatch]);
More Details,
Your Month reducer returns '' if the type of the action is not selectedMonth. So when the action is setLoading or removeLoading, the month gets setback to an empty string
change your Month reducer to
export const Month = (state = '', action) =>
action.type === 'selectedMonth' ? action.payload : state;
I am running into an issue with state changing and component not updating. All my research is pointing to mutate state improperly but I am using redux-toolkit to mutate change so I am unsure where the problem is. I am using typescript for my code.
I can confirm that state is mutating in 3 phases. When the component is initially rendered the useEffect hook kicks off, state is changed via setting isLoading to true. When the API call completes, the state is updated again causing the component to re-render. I am only selecting specific items from the state for now - ingredients and pagination.
The odd part is that the component correctly displays the list of ingredients but the value for pagecount (in pagination property from state) is not being picked up.
Specifically these two lines of code come across as undefined. When I log the entire object out ({console.log(pagination)}) I do see the correct values being logged after re-renders. Why is the property for pagination coming through as undefined between all re-renders? It's even undefined at the beginning of page load (before useEffect hook) even though its explicitly set in initial state.
<Pagination count={pagination.totalPages}
<p>page count: {pagination.totalPages}</p>
Above
const ingredients: Ingredients[] = [];
const pagination: XPaginationDto = {
currentPage: 1,
pageSize: 5,
totalCount: 0,
totalPages: 0,
nextPageLink: "",
previousPageLink: "",
};
const initialState = {
ingredients,
isLoading: false,
errorMessage: "",
pagination,
};
export const getIngredientsAction = createAsyncThunk(
"ingredients/getIngredients",
async (options: RequestOptionDto, { dispatch }) => {
try {
const response = await ingredientsApi.getIngredients(options);
return response;
} catch (e) {
dispatch(getIngredientsFailure(e));
}
}
);
const ingredientSlice = createSlice({
name: "ingredients",
initialState,
reducers: {
getIngredientsSuccess: (
state,
action: PayloadAction<PaginatedIngredients>
) => {
state.ingredients = action.payload.ingredients;
state.pagination = action.payload.xPaginationDto;
},
getIngredientsFailure: (state, action: PayloadAction<string>) => {
state.isLoading = false;
state.errorMessage = action.payload;
},
},
extraReducers: {
// #ts-ignore
[getIngredientsAction.pending]: (state, action) => {
state.isLoading = true;
},
// #ts-ignore
[getIngredientsAction.fulfilled]: (state, action: any) => {
const paginatedIngredients = action.payload as PaginatedIngredients;
state.isLoading = false;
state.ingredients = paginatedIngredients.ingredients;
state.pagination = paginatedIngredients.xPaginationDto;
},
// #ts-ignore
[getIngredientsAction.rejected]: (state, action) => {
state.isLoading = false;
},
},
});
My state setup is above. My component setup is fairly simple as well:
const IngredientsList: React.FC = () => {
const dispatch = useAppDispatch();
const { ingredients, pagination, renderCount } = useSelector(
(state: RootState) => state.ingredients
);
const [page, setPage] = React.useState(1);
const handlePagination =//some function to handle pagination
useEffect(() => {
console.log("test");
dispatch(
getIngredientsAction({
pageNumber: 1,
pageSize: 5,
sorts: "lastupdated",
} as RequestOptionDto)
);
}, [dispatch, page]);
return (
<>
<p>pagination value: {console.log(pagination)}</p>
<p>page count: {pagination.totalPages}</p>
{ingredients.map((x, index) => {
return (
<some component>
);
})}
</Grid>
<div>
<Typography>Page: {page}</Typography>
<Pagination
count={pagination.totalPages}
page={page}
variant="outlined"
onChange={handlePagination}
color="secondary"
/>
</div>
</>
);
};
export default IngredientsList;
I feel like an idiot. So all my code above is correct. I didn't parse the value for pagination from my API into an actual object so it was coming through as a string instead of an actual object. As soon as I fixed that it resolved the issue.
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));
}