Delete an object from object array {} - javascript

I'm trying to build a 'to-do' list for a task. Initial state must have the structure shown on code. I'm new to coding and cannot figure out how to delete an object from an object array.
I have tried using the .pop() and .filter() methods but they are not accepted because the object array is an object of objects and not an actual array. I also tried to find the index and do delete state.data[index] but the console sends an error message "cannot update component while rendering other component". Rest of the code works fine when I don't include the handleDeleteClick() function and remove the deleteItem reducer. Here's the code:
//the following creates an item component for each item in the 'to do' list
import React, {useState} from 'react';
import { useDispatch } from 'react-redux';
import { editItem, deleteItem, completedItem } from '../store/todoSlice';
const TodoItem = ({ id, content, completed }) => {
const dispatch = useDispatch();
//initialising state for items that the user wants to edit
const [edit, setEdit] = useState(false);
const [newItem, setNewItem] = useState(content);
const [finishedItem, setFinishedItem]= useState(false);
//function to call deleteItem reducer
const handleDeleteClick = () => {
dispatch(deleteItem({id, content}))
}
//function to call editItem reducer
const onSubmit = (event) => {
event.preventDefault();
dispatch(
editItem({id, newItem, completed}));
//setting edit and finished state back to null
setNewItem("");
setEdit(false);
setFinishedItem(false);
};
//function to call completedItem reducer
const completedTask = (event) => {
event.preventDefault();
dispatch(
completedItem({id, content, completed})
);
setFinishedItem(true);
};
//if edit state is true, return <input> element to edit the item requested
if(edit){
return (
<form>
<input id="editInput" value={newItem} onChange= {(e) => setNewItem(e.target.value)} placeholder="Edit your item"/>
<button onClick = {onSubmit} type='submit' id="submitButton">ADD</button>
</form>
)
}
//if edit state is false and finishedItem is true, return same list and add an id to completed button
if(!edit && finishedItem) {
return(
<div id="itemSection">
<li id="item">{content}
<button onClick= {handleDeleteClick(content)} className="function">DELETE</button>
<button onClick={() => setEdit(true)} className="function"> EDIT</button>
<button onClick={completedTask} id="completed">COMPLETED</button>
</li>
</div>
)
}
//else, return <ul> element for each 'todo' item with 3 buttons to edit, delete or complete task
return (
<div id="itemSection">
<li id="item">{content}
<button onClick={handleDeleteClick()} className="function">DELETE</button>
<button onClick={() => setEdit(true)} className="function"> EDIT</button>
<button onClick={completedTask}>COMPLETED</button>
</li>
</div>
);
};
export default TodoItem;
//the following creates state slice for the todos object array
import { createSlice } from "#reduxjs/toolkit";
export const toDoSlice = createSlice({
name: "todos",
//set initial state of object array
initialState: {
nextId: 2,
data:
{
1: {
content: 'Content 1',
completed: false
}
}
},
reducers: {
//function to add item to object array
addItem: (state, action) => {
state.data =
{
...state.data,
[state.nextId]: {
content: action.payload.content,
completed: false
}
}
state.nextId += 1;
},
//function to delete item from object array
deleteItem: (state, action) => {
const index= action.payload.id;
delete state.data[index];
},
//function to edit item from object array
editItem: (state, action) => {
state.data =
{
...state.data,
[action.payload.id]: {
content: action.payload.newItem,
completed: false
}
}
},
//function to complete item from object array
completedItem: (state, action) => {
state.data =
{
...state.data,
[action.payload.id]: {
content: action.payload.content,
completed: true
}
}
}
}
});
export const {addItem, editItem, deleteItem, completedItem} =
toDoSlice.actions;
export default toDoSlice.reducer;

The problem with your example is that you're setting up data as an object. You should not do that, unless you have a good reason to, which doesn't seem to be the case.
Instead of:
createSlice({
//...
initialState: {
nextId: 2,
data: { // 👈 object
1: {
content: 'Content 1',
completed: false
}
}
}
// ...
})
you should use:
createSlice({
// ...
initialState: {
nextId: 2,
data: [{ // 👈 array
content: 'Content 1',
completed: false
}]
}
// ...
})
Now data has all the array methods available, including .filter(). 1
If, for whatever reason, you want to keep data as an object, you could use
delete data[key]
where key is the object property you want to delete. (e.g: if you want to delete 1, use delete state.data.1 or delete state.data['1']).
But my strong advice is to change data to an array.
Notes:
1 - Note you will need to modify all your reducers to deal with the array. For example:
{
reducers: {
addItem: (state, action) => {
state.data.push({
content: action.payload.content,
completed: false
})
}
}
}
Most likely, you won't need state.nextId anymore. That's the advantage of dealing with arrays, you don't need to know what key/index you're assigning to when you add an item.
You will likely need to add an unique identifier to each item (e.g: an id) so you can find it by that id when you want to delete or modify it.

Related

How do I update the quantity without duplicate in redux?

I have the add and Remove items state and it sends to the cart. Im my cart, when I add, it duplicates. Do I need to create a reducer for this increment so it can update from 1 to 2 instead of duplicating it?
const INITIAL_STATE = []
export const addItem = createAction('ADD_ITEM')
export const increment = createAction('INCREMENT')
export const removeItem = createAction('REMOVE_ITEM')
export default createReducer(INITIAL_STATE, {
[addItem.type]: (state, action) => [...state, action.payload],
[removeItem.type]: (state, action) => state.filter(item => item._id !== action.payload._id)
})
Here is my products:
const INITIAL_STATE = []
export const addProducts = createAction('ADD_PRODUCTS')
export const addProduct = createAction('ADD_PRODUCT')
export default createReducer(INITIAL_STATE, {
[addProducts.type]: (state , action) => [...state, action.payload],
[addProduct.type]: (state, action) => [...action.payload]
})
My reducer:
export default configureStore({
reducer: {
products: productsReducer,
cart: cartReducer
}
})
It would be necessary that your reducer verify if the item ID exists or not. Case exists, update the item count. Instead of, create a new item with count equal to 1:
export default createReducer(INITIAL_STATE, {
[addItem.type]: (state, action) => {
const pos = state.map((i) => i._id).indexOf(action.payload._id);
if (pos !== -1) {
state[pos] = { ...action.payload, count: state[pos].count + 1 };
} else {
state.push({ ...action.payload, count: 1 });
}
},
[removeItem.type]: (state, action) =>
state.filter((item) => item._id !== action.payload._id),
});
Quantity
Right now your cart state is saving an array of all of the items that are in the cart. There is no quantity property anywhere. One thing that you could do is add a quantity: 1 property to the item object when you add that item to the cart. But...
Normalizing State
You don't really need to save the while item object because you already have that information in products. All that you really need to know is the id of each item in the cart and its quantity. The most logical data structure for that is a dictionary object where the keys are the item ids and the values are the corresponding quantities.
Since you only need the id I would recommend that your action creators addItem, removeItem and increment should just be a function of the id instead of the whole item. This means that you would call them like dispatch(addItem(5)) and your action.payload would just be the id. The addProduct and addProducts actions still need the whole item object.
An array of products is fine and you can always call state.products.find(item => item._id === someId) to find a product by its id. But a keyed object is better! You want to have the keys be the item ids and the values be the corresponding objects. Redux Toolkit has built-in support for this structure with the createEntityAdapter helper.
Create Slice
There's not any problem with defining your actions though createAction, but it's not needed. You can replace createAction and createReducer with createSlice which combines the functionality of both.
Here's another way of writing your products:
import { createSlice, createEntityAdapter } from "#reduxjs/toolkit";
const productsAdapter = createEntityAdapter({
selectId: (item) => item._id
});
const productsSlice = createSlice({
// this becomes the prefix for the action names
name: "products",
// use the initial state from the adapter
initialState: productsAdapter.getInitialState(),
// your case reducers will create actions automatically
reducers: {
// you can just pass functions from the adapter
addProduct: productsAdapter.addOne,
addProducts: productsAdapter.addMany
// you can also add delete and update actions easily
}
});
export default productsSlice.reducer;
export const { addProduct, addProducts } = productsSlice.actions;
// the adapter also creates selectors
const productSelectors = productsAdapter.getSelectors(
// you need to provide the location of the products relative to the root state
(state) => state.products
);
// you can export the selectors individually
export const {
selectById: selectProductById,
selectAll: selectAllProducts
} = productSelectors;
And your cart:
import {createSlice, createSelector} from "#reduxjs/toolkit";
const cartSlice = createSlice({
name: 'cart',
// an empty dictionary object
initialState: {},
reducers: {
addItem: (state, action) => {
const id = action.payload;
// add to the state with a quantity of 1
state[id] = 1;
// you might want to see if if already exists before adding
},
removeItem: (state, action) => {
const id = action.payload;
// you can just use the delete keyword to remove it from the draft
delete state[id];
},
increment: (state, action) => {
const id = action.payload;
// if you KNOW that the item is already in the state then you can do this
state[id]++;
// but it's safer to do this
// state[id] = (state[id] || 0) + 1
}
}
})
export default cartSlice.reducer;
export const {addItem, removeItem, increment} = cartSlice.actions;
// you can select the data in any format
export const selectCartItems = createSelector(
// only re-calculate when this value changes
state => state.cart,
// reformat into an an array of objects with propeties id and quantity
(cart) => Object.entries(cart).map(([id, quantity]) => ({id, quantity}))
)
// select the quantity for a particular item by id
export const selectQuantityById = (state, id) => state.cart[id]
// you can combine the ids with the products, but
// I actually recommend that you just return the ids and get the
// product data from a Product component like <Product id={5} quantity={2}/>
export const selectCartProducts = createSelector(
// has two input selectors
state => state.cart,
state => state.products,
// combine and reformat into an array of objects
(cart, products) => Object.keys(cart).map(id => ({
// all properties of the product
...products.entries[id],
// added quantity property
quantity: cart[id],
}))
)

React toggle button only works once?

I am learning React Reducer now. I want to build a toggle button that changes a boolean completed value to its opposite each time I click the button.
What I have is an array of states, each state is an object with an id and a completed value set to be true or false. Then I loop through states, setting each state as an Item component and display it on screen.
// App.js file
import React, { useReducer } from "react";
import { AppReducer } from "./AppReducer";
import Item from "./Item";
function App() {
const initialStates = [
{
id: 1,
completed: false,
},
{
id: 2,
completed: false,
},
];
const [states, dispatch] = useReducer(AppReducer, initialStates);
return (
<div>
{states.map((state) => (
<Item item={state} key={state.id} dispatch={dispatch} />
))}
</div>
);
}
export default App;
In the Item component, I display whether this item is completed or not (true or false). I set up a toggle function on the button to change the completed state of the Item.
// Item.js
import React from "react";
const Item = ({ item, dispatch }) => {
function setButtonText(isCompleted) {
return isCompleted ? "True" : "False";
}
let text = setButtonText(item.completed);
function toggle(id){
dispatch({
type: 'toggle',
payload: id
})
text = setButtonText(item.completed);
}
return (
<div>
<button type="button" onClick={() => toggle(item.id)}>Toggle</button>
<span>{text}</span>
</div>
);
};
export default Item;
Here is my reducer function. Basically what I am doing is just loop through the states array and locate the state by id, then set the completed value to its opposite one.
// AppReducer.js
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const newStates = states;
for (const state of newStates) {
if (state.id === action.payload) {
const next = !state.completed;
state.completed = next;
break;
}
}
return [...newStates];
}
default:
return states;
}
};
So my problem is that the toggle button only works once. I checked my AppReducer function, it did change completed to its opposite value, however, every time we return [...newStates], it turned back to its previous value. I am not sure why is that. I appreciate it if you can give it a look and help me.
The code is available here.
Here is the working version forked from your codesandbox
https://codesandbox.io/s/toggle-button-forked-jy6jd?file=/src/Item.js
The store value updated successfully. The problem is the way of listening the new item change.
dispatch is a async event, there is no guarantee the updated item will be available right after dispatch()
So the 1st thing to do is to monitor item.completed change:
useEffect(() => {
setText(setButtonText(item.completed));
}, [item.completed]);
The 2nd thing is text = setButtonText(item.completed);, it will not trigger re-render. Therefore, convert the text to state and set it when item.completed to allow latest value to be displayed on screen
const [text, setText] = useState(setButtonText(item.completed));
I have improved your code, just replace your AppReducer code with below.
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const updated = states.map((state) =>
action.payload === state.id ? {
...state,
completed: !state.completed
} : { ...state }
);
return [...updated];
}
default:
return states;
}
};
Live demo

React Redux update item quantity (more than just one increment)

I have a site where the user can increase the quantity on the product before adding it to cart. Now if the user decided to go back to the product and add 3 more by increasing the quantity on the product, then adding to cart - how do I update the quantity of the existing product in basket?
At the moment I get duplicates of the product with different quantities depending on what is selected.
Here is the code I have for my reducer:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
items: [],
};
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
// No idea what to do with this..
state.items.filter((pizza) => pizza.name === payload.name);
// This pushes the item fine, but I get multiple of the same item in the cart instead of just updating its quantity
state.items.push(payload);
// state.items.map((pizza) =>
// pizza.name === payload.name
// ? {
// ...pizza,
// quantity: pizza.quantity + payload.quantity,
// }
// : pizza
// );
},
},
});
export const { addToBasket } = basket.actions;
export const basketItems = (state) => state.basket.items;
export default basket.reducer;
The payload is the specific product, it will be an object:
{
name: "product name",
image: "url.jpeg",
price: "14.99"
}
I can not for the life of me figure out what to do here in order not to mutate the state. Nothing works, I feel like I have tried every possible way but clearly I am missing something.
Any help much appreciated!!!
Thanks
You actually have all the code you need, just need it applied correctly. First check that the item is already in the items array or not. If it is already there then copy the existing state and update the matching element. If it is not included then append the new item to the end of the array.
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
const item = state.items.find((pizza) => pizza.name === payload.name);
if (item) {
state = state.items.map((pizza) =>
pizza.name === payload.name
? {
...pizza,
quantity: pizza.quantity + payload.quantity
}
: pizza
);
} else {
state.items.push(payload);
}
},
},
});
With Redux-Toolkit you can mutate the state objects so you can likely simplify this a bit. Instead of mapping a new array and setting it back to state, jut mutate the found object.
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
const item = state.items.find((pizza) => pizza.name === payload.name);
if (item) {
item.quantity += payload.quantity;
} else {
state.items.push(payload);
}
},
},
});

How can I use Redux to only update one instance of a component?

I'm trying to use Redux to update my Card Component to disable and change colors on click. Redux dispatches the action fine, but it updates all Cards not just the one that was clicked. Each Card has an object associated with it that hold the word and a value. The value is the className I want to use to change the color when clicked
Component
const Card = ({ wordObj, updateClass, isDisabled, cardClass }) => {
const showColor = (e) => {
updateClass(wordObj);
console.log(cardClass)
};
return (
<button
onClick={(e) => showColor()}
disabled={isDisabled}
className={cardClass}>
{wordObj.word}
</button>
);
};
const mapStateToProps = (state) => ({
cardClass: state.game.cardClass,
});
export default connect(mapStateToProps, { updateClass })(Card);
Action
export const updateClass = (obj) => (dispatch) => {
console.log(obj)
dispatch({
type: UPDATE_CARD,
payload: obj,
});
};
Reducer
const initialState = {
words: [],
cardClass: 'card',
isDisabled: false,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_WORDS: {
return {
...state,
words: payload,
};
}
case UPDATE_CARD:
return {
...state,
isDisabled: true,
cardClass: ['card', payload.value].join(' '),
};
default:
return state;
}
}```
All of your card components are consuming the same cardClass field in the state. When you modify it in this line:
cardClass: ['card', payload.value].join(' ')
All cards that are consuming this field have their classes updated. The same occurs to the isDisable field.
You need to create one object for each card in your state. Here is my implementation (was not tested):
const initialState = {
cards: []
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
// create a card object for each word
case SET_WORDS: {
return {
...state,
cards: payload.map(word => {
return { word: word, cardClass: "card", isDisabled: false }
})
};
}
case UPDATE_CARD:
// here i'm using the wordObj.word passed as payload
// to identify the card (i recommend to use an id field)
const cardIndex = state.cards.findIndex(card => card.word === payload.word);
// get the current card
const card = state.cards[cardIndex];
// create an updated card object obeying the immutability principle
const updatedCard = { ...card, isDisabled: true, cardClass: ['card', payload.value].join(' '), }
return {
...state,
cards: [
...state.cards.slice(0, cardIndex), // cards before
updatedCard,
...state.cards.slice(cardIndex + 1) // cards after
]
};
default:
return state;
}
}
Your mapStateToProps selects a string, but said string changes on any updateClass and that causes all your cards to update, because the selection of state.game.cardClass produces a different value, which triggers a new render for the connected component.
Maybe what you want, is something that identifies the selection, i.e. an id for each card, and select with that id in the mapStateToProps to avoid reading the change, because what's happening right now is the following:
Card A[className="card A"] == after dispatch ==> mapStateToProps => [className="card B"]
Card B[className="card A"] => dispatch('B') => mapStateToProps => [className="card B"]
B is updating the state of both A and B, and that's why the extra render occurs

Initial default state is not showing, displaying empty

This is from a tutorial assignment from Dave Ceddia's Redux course, I am trying to display the initial state, which contains an array of objects, however it is simply returning undefined and not displaying anything. I am new to React, and I have hit a wall on getting 1) my buttons to display the state, and 2) default state to appear initially.
I have tried to have my component Buttons as a class, and constant.
I have tried stating my initialReducer in the default: return state; in my reducer as well. I have also tried different syntax for my dispatch actions, but nothing seems to be getting to the reducer.
index.js
import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import { getAllItems, addEventToBeginning, addEventToEnd } from "./actions";
import { connect, Provider } from "react-redux";
import { store } from "./reducers";
const Buttons = ({
state,
getAllItems,
addEventToBeginning,
addEventToEnd
}) => (
<React.Fragment>
<ul>{state ? state.actions.map(item => <li>{item}</li>) : []}</ul>
<button onClick={getAllItems}> Display items </button>
<button onClick={addEventToBeginning}> addEventToBeginning </button>
<button onClick={addEventToEnd}> addEventToEnd </button>
</React.Fragment>
);
const mapDispatchToProps = { getAllItems, addEventToBeginning, addEventToEnd };
const mapStateToProps = state => ({
actions: state.actions,
sum: state.sum
});
connect(
mapStateToProps,
mapDispatchToProps
)(Buttons);
reducers.js
const initialState = {
actions: [
{ id: 0, type: "SALE", value: 3.99 },
{ id: 1, type: "REFUND", value: -1.99 },
{ id: 2, type: "SALE", value: 17.49 }
],
sum: 0
};
const newUnit = { id: Math.random * 10, type: "SALE", value: Math.random * 25 };
function eventReducer(state = initialState, action) {
switch (action.type) {
case ADD_EVENT_TO_BEGINNING:
const copy = { ...state };
copy.actions.unshift(newUnit);
return copy;
case ADD_EVENT_TO_END:
const copy2 = { ...state };
copy2.actions.unshift(newUnit);
return copy2;
cut out for cleanliness
case GET_ITEMS:
return {
...state,
actions: state.actions,
sum: state.sum
};
default:
return state;
}
}
export const store = createStore(eventReducer);
example of actions.js (they all follow same format)
export const ADD_EVENT_TO_BEGINNING = "ADD_EVENT_TO_BEGINNING";
export function addEventToBeginning() {
return dispatch => {
dispatch({
type: ADD_EVENT_TO_BEGINNING
});
};
}
UPDATE:
Thank you #ravibagul91 and #Yurui_Zhang, I cut everything but getAllItems out, and changed the state to:
const initialState = {
itemsById: [
{ id: 0, type: "SALE", value: 3.99 },
{ id: 1, type: "REFUND", value: -1.99 },
{ id: 2, type: "SALE", value: 17.49 }
]
};
class Form extends React.Component {
render() {
return (
<div>
{this.props.itemsById
? this.props.itemsById.map(item => (
<li>
{item.id} {item.type} {item.value}
</li>
))
: []}
<button onClick={this.getAllItems}> Display items </button>
</div>
);
}
}
const mapDispatchToProps = { getAllItems };
function mapStateToProps(state) {
return {
itemsById: state.itemsById
};
}
export function getAllItems() {
return dispatch => ({
type: "GET_ITEMS"
});
}
There are multiple problems with your code:
const mapStateToProps = state => ({
actions: state.actions,
sum: state.sum
});
Here you have mapped redux state fields to props actions and sum - your component won't receive a state prop, instead it will receive actions and sum directly.
so your component really should be:
const Button = ({
actions,
sum,
}) => (
<>
<ul>{actions && actions.map(item => <li>{item}</li>)}</ul>
</>
);
your mapDispatchToProps function is not defined correctly. It should be something like this:
// ideally you don't want the function names in your component to be the same as the ones you imported so I'm renaming it here:
import { getAllItems as getAllItemsAction } from "./actions";
// you need to actually `dispatch` the action
const mapDispatchToProps = (dispatch) => ({
getAllItems: () => dispatch(getAllItemsAction()),
});
Your reducer doesn't seem to be defined correctly as well, however you should try to fix the problems I mentioned above first :)
Try not to do too much in one go when you are learning react/redux. I'd recommend reviewing the basics (how the data flow works, how to map state from the redux store to your component, what is an action-creator, etc.).
As you are destructuring the props,
const Buttons = ({
state,
getAllItems,
addEventToBeginning,
addEventToEnd
}) => ( ...
You don't have access to state, instead you need to directly use actions and sum like,
const Buttons = ({
actions, // get the actions directly
sum, // get the sum directly
getAllItems,
addEventToBeginning,
addEventToEnd
}) => (
<React.Fragment>
//You cannot print object directly, need to print some values like item.type / item.value
<ul>{actions && actions.length && actions.map(item => <li>{item.type} {item.value}</li>)}</ul>
<button onClick={getAllItems}> Display items </button>
<button onClick={addEventToBeginning}> addEventToBeginning </button>
<button onClick={addEventToEnd}> addEventToEnd </button>
</React.Fragment>
);
Your mapDispatchToProps should be,
const mapDispatchToProps = dispatch => {
return {
// dispatching actions returned by action creators
getAllItems : () => dispatch(getAllItems()),
addEventToBeginning : () => dispatch(addEventToBeginning()),
addEventToEnd : () => dispatch(addEventToEnd())
}
}
Or you can make use of bindActionCreators,
import { bindActionCreators } from 'redux'
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ getAllItems, addEventToBeginning, addEventToEnd }, dispatch)
}
}
In reducer, ADD_EVENT_TO_END should add element to end of the array, but you are adding again at the beginning using unshift. You should use push which will add element at the end of array,
case ADD_EVENT_TO_END:
const copy2 = { ...state };
copy2.actions.push(newUnit); //Add element at the end
return copy2;
Also your GET_ITEMS should be as simple as,
case GET_ITEMS:
return state;

Categories

Resources