I need to toggle somehove adding/removing object from redux store. It is like check/uncheck. I have following code:
const list = ({ id, item }) => {
const isChecked = name => items.some(item => item.name === name);
let itemClass = cx({
item: true,
selected: isChecked(name),
});
return (
<li className={itemClass}
onClick={() => click(fullItem)} key={id}>
<div className={styles.name}>
{isChecked(name) ?
(<span><i className={`fa fa-check`}></i>{name}</span>)
: (<span>{name}</span>)
}
</div>
</li>
);
}
export const click = item => ({
type: ADD_ITEM,
payload: item,
});
import {
ADD_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_ITEM:
return {
...state,
items: [action.payload],
};
default:
return state;
}
};
but for now it only work for adding item to store, when I click on item when it is selected, it should remove it from the store. How can I toggle onclick removing/adding object to redux store?
You could try something like this. Changing the ADD_ITEM instead to a TOGGLE_ITEM where you check for existence of the item using something like Array.prototype.find. Adding if it does not exist, and removing it if it does exist:
export const click = item => ({
type: TOGGLE_ITEM,
payload: item,
});
import {
TOGGLE_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_ITEM:
const currentItem = state.items.find(item => item.id === action.payload.id);
if (!currentItem) {
return {
...state,
items: [...state.items, action.payload],
};
} else {
const newItems = state.items.filter(item => item.id !== action.payload.id];
return {
...state,
items: [...newItems]
};
}
default:
return state;
}
};
You may though want to consider having separate add, update, and delete actions, and dispatch the different actions accordingly from your components.
export const click = item => ({
type: TOGGLE_ITEM,
payload: item,
});
import {
TOGGLE_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_ITEM:
// check to see if the item already in our array
// Array.some return true/false
const itemAlreadyExists = state.items.some(item => item.id === action.payload.id)
return {
...state,
// if the item already in our array filter it
// if not just add it
items: itemAlreadyExists
? state.items.filter(item => item.id !== action.payload.id)
: [...state.items, action.payload],
};
default:
return state;
}
};
Related
Error type: Store does not have a valid reducer. Make sure the
argument passed to combineReducers is an object whose values are
reducers.
I am trying to add a Store and my app is failing. I don't understand where the error is coming from. Can someone point out my error and how to fix it?
store
import { configureStore } from '#reduxjs/toolkit';
import rootReducers from './reducers';
const store = configureStore({
reducer: rootReducers,
});
export default store;
reducer
import handleCart from './handleCart';
import { combineReducers } from 'redux';
const rootReducers = combineReducers({
handleCart,
});
export default rootReducers;
const cart = [];
const handleCart = (state = cart, action) => {
const product = action.payload;
switch (action.type) {
case ADDITEM:
// Check product exist
const exist = state.find(x => x.id === product.id);
if (exist) {
return state.map(x =>
x.id === product.id ? {...x, qty: x.qty + 1} : x,
);
} else {
const product = action.payload;
return [
...state,
{
...product,
qty: 1,
},
];
}
break;
case REMOVEITEM:
const exist1 = state.find(x => x.id === product.id);
if (exist1.qty === 1) {
return state.filter(x => x.id !== exist1.id);
} else {
return state.map(x =>
x.id === product.id ? {...x, qty: x.qty - 1} : x,
);
}
break;
default:
break;
}
};
I see only a couple overt issues in the handleCart reducer function code, either could be cause for throwing an exception. It seems the handleCart reducer function is missing returning state in the default case. The REMOVEITEM case also incorrectly assumes exist is defined and accesses a qty property of a potentially undefined object. Remember that Array.prototype.find returns undefined when no matching element is found in the array.
Make sure the handleCart is default exported so it can be imported to create the root reducer.
const cart = [];
const handleCart = (state = cart, action) => {
const product = action.payload;
switch (action.type) {
case ADDITEM: {
// Check product exist
const exist = state.find(x => x.id === product.id);
if (exist) {
return state.map(x =>
x.id === product.id ? { ...x, qty: x.qty + 1 } : x,
);
}
return [...state, { ...product, qty: 1 }];
}
case REMOVEITEM: {
const exist = state.find(x => x.id === product.id);
if (exist) {
// item exists, update quantity
if (exist.qty === 1) {
return state.filter(x => x.id !== exist1.id);
} else {
return state.map(x =>
x.id === product.id ? { ...x, qty: x.qty - 1 } : x,
);
}
}
// item didn't exist, just return current state
return state;
}
default:
// No action to do, return current state
return state;
}
};
export default handleCart;
You are using redux-toolkit, why are you not creating state slice? Here's a similar implementation using createSlice. This allows you to write the reducer functions using mutable updates instead of needing to shallow copy all the parts of the state that are being updated.
import { createSlice } from '#reduxjs/toolkit';
const initialState = [];
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addItem: (state, action) => {
const product = state.find(product => product.id === action.payload.id);
if (product) {
product.qty++;
} else {
state.push({ ...action.payload, qty: 1 });
}
},
removeItem: (state, action) => {
const product = state.find(product => product.id === action.payload.id);
if (product) {
product.qty--;
if (product.qty === 0) {
state.filter(product => product.id !== action.payload.id);
}
}
},
},
});
export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;
so my reducer looks like that.
I want to add items to cart and if the item is already in the state it should increase the qty of that item.
I pass object to payload but it doesnt contain the quantity field so i create it manually.
But when i add more than 2 of the same item it doesnt update further.
What is the issue here?
const init = { cartItems: [] };
export const cartReducer = (state = init, action) => {
switch (action.type) {
case 'ADD_TO_CART': {
const theItem = state.cartItems.find(
(item) => item.title === action.payload.title
);
if (theItem) {
theItem.quantity++;
return { ...state };
} else {
const updatedCart = [...state.cartItems, action.payload];
return { ...state, cartItems: updatedCart };
}
}
default: {
return { ...state };
}
}
};
You cannot mutate the state, it will then not update the react component, you can try the below code for updating the quantity
const init = { cartItems: [] };
export const cartReducer = (state = init, action) => {
switch (action.type) {
case 'ADD_TO_CART': {
const itemIndex = state.cartItems.findIndex(
(item) => item.title === action.payload.title
);
if (itemIndex !== -1) {
const newCartItems = [...state.cartItems];
const newItem = {...newCartItems[itemIndex]}
newItem.quantity += 1;
newCartItems[itemIndex] = newItem;
return { ...state, cartItems: newCartItems};
} else {
const updatedCart = [...state.cartItems, action.payload];
return { ...state, cartItems: updatedCart };
}
}
default: {
return { ...state };
}
}
};
EDIT: The other option you can use is with Immer js it makes state management easier, though it will add in bundle size, if you have strict budget of bundle size then you would need to think of adding Immer js, Below how you can write the logic with Immer js.
import produce from 'immer'
const init = { cartItems: [] };
export const cartReducer = (state = init, action) =>
produce(state, draft => {
switch (action.type) {
case 'ADD_TO_CART': {
const itemIndex = draft.cartItems.findIndex(
(item) => item.title === action.payload.title
);
if (itemIndex !== -1) {
draft.cartItems[itemIndex].quantity += 1;
} else {
draft.cartItems.push(action.payload)
}
break;
}
default: {
break;
}
}
})
Give it a try, Immer js Documentation, Redux and Immer
As an initial state I use array of objects:
export default{
items: [
{
Date: 1,
Operation: 'revenue',
}
]
}
In component I dispatch the action, which must update one element in object of array: "Operation"
const mapDispatchToProps = (dispatch) => {
return {
selectOperation: (input) => dispatch({type: app.SELECT, payload: input})
}
};
class OperationSelect extends React.Component {
// constructor
handleChange(event) {
this.props.selectOperation({
key: 'Operation',
value: event.target.value
});
};
// render() logic
}
export default connect(null, mapDispatchToProps)(OperationSelect)
Reducer:
import initialState from '../constants/initialState';
import { app } from '../constants/types';
export default function update(state = initialState, action) {
switch (action.type) {
case app.SELECT:
return {
...state,
[action.payload.key]: state.items.map(
(item, i)=> i===0 ? {...item, Operation: action.payload.value}
: item
)
};
default:
return state;
}
}
But, when I select new value and application run handleChange, dispatch the action, run reducer, the state in store keeps the old value of "Operation".
What am I doing wrong?
This is I think what you need to do:
first add an id property to your items and then do something like this:
export default {
items: [
{
id: 0,
Date: 1,
Operation: "revenue",
},
],
};
class OperationSelect extends React.Component {
// constructor
handleChange(event) {
this.props.selectOperation({
key: "Operation", // I think you need to check this and try to findout that you need this or not
value: event.target.value,
id: 0 // 0 is just an example you need to decide how you would implement the id
});
}
// render() logic
}
export default function update(state = initialState, action) {
switch (action.type) {
case app.SELECT:
return {
...state,
items: state.items.map((item, i) =>
i === action.payload.id ? { ...item, Operation: action.payload.value } : item
),
};
default:
return state;
}
}
The problem was in that I did not update in reducer the state of array.
This is a working code:
export default function update(state = initialState, action) {
switch (action.type) {
case app.SELECT:
return {
...state,
items: state.items.map(
(item, i)=> i===0 ? {...item, [action.payload.key]: action.payload.value}
: item
)
};
default:
return state;
}
}
Earlier in return-block I used [action.payload.key] in place, where "items" should have used. So I updated "Operation" in place, where "items" updated.
I know that redux trigger react component to re-render once state of that component get changed but this doesn't happen in my situation.
Action
const addToCart = (product, cartProducts) => {
let newCartProducts = [...cartProducts, product];
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeated = acc.find(p => p.id === cur.id);
if(repeated) repeated.quantity++;
else acc.push(cur);
return acc;
}, []);
return {
type: ADD_TO_CART,
payload: newCartProducts
}
}
Reducer:
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
return action.payload;
default:
return state;
}
}
The reducer returns a new state every time the action dispatched from the component but i need to close the cart and open it again to get the effect, redux does not update the product quantity simultaneously??
You are modifying the existing elements in the state.
Use
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeatedIndex = acc.findIndex(p => p.id === cur.id);
const repeated = acc[repeatedIndex];
if (repeated) {
acc[repeatedIndex] = {
...repeated,
quantity: repeated.quantity + 1
};
} else acc.push(cur);
return acc;
}, []);
You array is recreated each time, but the objects inside it are not. So when you modify their internals you need to notify that the specific object has changed.
Refactor logic to the reducer and set the quantity here:
const addToCart = product => {
return {
type: ADD_TO_CART,
payload: product,
};
};
//I assume state is an array of products on your cart
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
const { id } = action.payload;
return state.map(p => p.id).includes(id)
? //product is already on card add quanity
state.map(p =>
p.id === id
? { ...p, quantity: p.quantity + 1 }
: p
)
: state.concat({ ...action.payload, quantity: 1 }); // add product
default:
return state;
}
};
I have a list of products called work items stored on my Redux store and I want to add an action that adds new work item or remove existing one when user picks up a a work item from the ui.
What I have so far is this workItemReducer:
import {
FETCH_WORKITEMS_BEGIN,
FETCH_WORKITEMS_SUCCESS,
FETCH_WORKITEMS_FAILURE,
SELECTED_WORKITEM
} from '../actions/workItemAction';
const initialState = {
workItems: [{"name":'work 1'}, {"name":'work 2'}, {"name":'work 3'}],
workItemsSelected: {},
loading: false,
error: null
};
export default function workItemReducer(state = initialState, action) {
switch(action.type) {
case FETCH_WORKITEMS_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_WORKITEMS_SUCCESS:
return {
...state,
loading: false,
workItems: action.payload.workItems
};
case FETCH_WORKITEMS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
workItems: []
};
case SELECTED_WORKITEM:
return {
...state,
workItemsSelected: action.payload.workItem
};
default:
return state;
}
}
and the actions looks as below:
export const FETCH_WORKITEMS_BEGIN = 'FETCH_WORKITEMS_BEGIN';
export const FETCH_WORKITEMS_SUCCESS = 'FETCH_WORKITEMS_SUCCESS';
export const FETCH_WORKITEMS_FAILURE = 'FETCH_WORKITEMS_FAILURE';
export const SELECTED_WORKITEM = 'SELECTED_WORKITEM';
export const fetchWorkItemsBegin = () => ({
type: FETCH_WORKITEMS_BEGIN
});
export const fetchWorkItemsSuccess = workItems => ({
type: FETCH_WORKITEMS_SUCCESS,
payload: { workItems }
});
export const fetchWorkItemsFailure = error => ({
type: FETCH_WORKITEMS_FAILURE,
payload: { error }
});
export const selectedWorkItem = workItem => ({
type: SELECTED_WORKITEM,
payload: { workItem }
});
I have a container component that disptach or call these actions which I am a bit confused where the logic of adding a new one or removing existing one happens, either on the container/smart component or directly in the reducer.
Container component has this method:
onWorkItemSelect = (workItem) => {
this.props.dispatch(selectedWorkItem(workItem));
};
Anyone can help on writing the logic of adding new or remove existing one and where that code should live?
adding this to reducer works thou im not sure if all this code should remain into the reducer:
case SELECTED_WORKITEM:
let arr = [];
if (containsObject(action.payload.workItem, state.workItemsSelected)) {
arr = remove(state.workItemsSelected, action.payload.workItem);
} else {
arr = [...state.workItemsSelected, action.payload.workItem];
}
return {
...state,
workItemsSelected: arr
};
It should be done in the reducer
when adding one you could just spread the current array which you can get from the reducer state
const { workItems } = state;
const { workItem } = action.payload;
return {
// ...other stuff to return
workItems: [...workItems, workItem],
}
to delete one
const { workItems } = state;
const { workItem } = action.payload;
return {
// ...other stuff to return
workItems: workItems.filter(x => x.name === workItem.name),
}