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;
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 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 am using useReducer in my context provider. The idea is that I will be a central place for my state and dispatch functions to live.
I am making an axios call to a datapase to fetch projects. However, when I return in the dispatch function, it is returning a promise. How can I return the data from the axios call so that it stores the data from the call in state?
const initState = []
const projectsReducer = async (state, action) => {
switch(action.type) {
case 'FETCH_PROJECTS':
const req = await axios.get('/api/fetch_projects')
const { data } = req
return {...state, data}
default:
return state
}
}
useEffect(() => {
const initFetch = () => {
projectsDispatch({type: 'FETCH_PROJECTS'})
}
initFetch()
}, [])
const [projects, projectsDispatch] = useReducer(projectsReducer, initState)
Do your fetch within the effect, then pass the data into the reducer.
A reducer is a pure function that should do no side effects. Plus whatever data is returned from the reducer is set as the next state. So, an async function always returns a promise - which means that you are setting the state of projects to be a promise for the data.
If you refactor your code as follows, it should work.
const initState = {data: []};
const projectsReducer = (state, action) => {
switch(action.type) {
case 'FETCH_PROJECTS':
const { data } = action.payload;
return {...state, data}
default:
return state
}
}
const [projects, projectsDispatch] = useReducer(projectsReducer, initState)
useEffect(() => {
const initFetch = async () => {
const req = await axios.get('/api/fetch_projects')
projectsDispatch({type: 'FETCH_PROJECTS', payload: {data: req.data}})
}
initFetch()
}, [])
// data is in projects.data;
Though since it's more simple, you don't really need a reducer:
const [projects, setProjects] = useState([]);
useEffect(() => {
const initFetch = async () => {
const req = await axios.get('/api/fetch_projects')
setProjects(req.data);
}
initFetch()
}, [])
// data is in projects
I would like to update Redux State on the basis of one action, as follows:
export const editAction = (itemType, itemContent, id) => (dispatch) => {
return axios.put(`${url}/${itemType}/${id}`, {
itemType,
...itemContent,
})
.then(({ data }) => {
console.log(data);
dispatch({
type: EDIT_SUCCESS,
itemType,
data,
id,
});
})
};
I omitted catch block to shorten source code.
What return should I use to update Redux State in reducer after EDIT_SUCCESS action type?
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_SUCCESS:
return {
...state,
[action.itemType]: [...action.data],
};
case ADD_SUCCESS:
return {
...state,
[action.itemType]: [...state[action.itemType], action.data],
};
case EDIT_SUCCESS:
return {
...state,
// ???,
};
case DELETE_SUCCESS:
return {
...state,
[action.itemType]: [...state[action.itemType].filter(item => item._id !== action.id)],
};
}
};
It is quite clear to me how to implement FETCH, ADD and DELETE because I done it (as you can see) but I have no idea how to implement return for EDIT_SUCCESS.
But importantly, after run editAction() (submit button in Formik), editing object is updated (by axios) in the database correctly but State in Redux DevTools as well as view in a browser remains the same. Only when I refresh page, I see the difference.
If you have edited an element, you should look for it and update it.
Something like:
const newState = {...state};
const indexOfElementToUpdate = newState[action.itemType].findIndex(item => item._id === action.id);
newState[action.itemType][indexOfElementToUpdate] = action.data;
return newState;