React Reducer Updates Twice - javascript

I understand that in Strict Mode, the reducer should run twice. However, it shouldn't actually update the values twice.
The quantity for items gets updated twice.
For example, there is an item, items: [{name: tshirt, price: 10, quantity: 1}] already in the cart. If call addItem(state, tshirt, 1), the cart will update to items: [{name: tshirt, price: 10, quantity: 3}].
Only the quantity value inside the items array is updated twice. The outside variables such as value and total_qty only update once.
How do I stop it updating twice without turning off StrictMode?
interface Product {
name: string,
materials: string[],
categories: string[],
price: number,
image?: string
}
interface ICartItem extends Product {
quantity: number
}
interface Cart {
items: {[key: string]: ICartItem},
value: number,
total_qty: number
}
const addItem = (state: Cart, product: Product, quantity: number) => {
let item = state?.items?.[product.name];
if (item) {
item.quantity += quantity;
} else {
item = {
...product,
quantity
}
}
let updatedCart = {
...state,
items: {
...state.items,
[product.name]: item
},
value: Math.max(0, state.value + (product.price * quantity)),
total_qty: Math.max(0, state.total_qty + quantity)
}
return updatedCart;
}
const cartReducer: Reducer<Cart, UpdateCartAction> = (state: Cart, action: UpdateCartAction) => {
switch (action.type) {
case 'ADD_ITEM':
return addItem(state, action.product, action.quantity);
case 'REMOVE_ITEM':
return removeItem(state, action.product, action.quantity);
case 'CLEAR_CART':
return clearCart();
default:
return state;
}
}
export const CartContext = React.createContext<ICartContext | undefined>(undefined);
export const CartProvider = ({children}: {children: ReactNode}) => {
const {cart, dispatch} = useLocalStorageReducer(
'cart',
cartReducer,
initialCart
);
const contextValue = useMemo(()=>{
return {cart, dispatch}
}, [cart]);
return (
<CartContext.Provider value={contextValue}>{children}</CartContext.Provider>
)
}
export const useCart = () => {
const contextValue = useContext(CartContext);
let cart: Cart | undefined, dispatch: Dispatch<any> | undefined;
if (contextValue) {
cart = contextValue.cart;
dispatch = contextValue.dispatch;
}
const addItem = (product: Product, quantity: number) => {
if (dispatch) dispatch({type: "ADD_ITEM", product, quantity});
}
return {
cart,
addItem
}
}

Related

InitialState not updated in React-Redux

I am trying to change the state immutably and return a new state but in the UI component new state not changed. The new state values are fetched successfully but not display. I don't understand what is the issue behind.
Anyone has suggestions share me
Here is my reducer:
import * as actionTypes from './actions';
const initialState = {
data: [
{id: 1, name: "accordion1", content: () => {}, status: 1},
{id: 2, name: "accordion2", content: () => {}, status: 0},
{id: 3, name: "accordion3", content: () => {}, status: 0},
]
}
const reducer = (state = initialState, action) => {
debugger;
switch(action.type) {
case actionTypes.ACTIVE_STATE:
debugger;
var newData = state.data;
for(var i= 0; i<newData.length; i++) {
newData[i].status = newData[i].status === 1 ? 0 : 1
}
return {
...state,
data: newData
}
default:
return state;
}
}
export default reducer;
Here is my UI component were not update:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actionTypes from '../store/actions';
class Accordion extends Component {
render() {
debugger;
return (
<div>
{this.props.accordions.map((accordion, index) => {
return (
<div key={index}>
<div>{accordion.status}</div>
<div className={`accordion ${accordion.status}`} onClick={this.props.expandAccordion}>
{accordion.name}
</div>
<div className="panel">
</div>
</div>
);
})}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
accordions: state.data
};
}
const mapDispatchToProps = (dispatch) => {
return {
expandAccordion: () => dispatch({type: actionTypes.ACTIVE_STATE})
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Accordion);
I assume that the problem is in the following lines:
var newData = state.data;
for(var i= 0; i<newData.length; i++) {
newData[i].status = newData[i].status === 1 ? 0 : 1
}
Why?
Since basically, when you assign var newData = state.data; you actually copy the object reference, and by that, you don't keep it immutable, and as far for React, which makes shallow comparing, it never changed.
One possible solution would be to change this code to an immutable update:
const newData = state.data.map((entry) => ({...entry, status: entry.status === 1 ? 0 : 1}));
return {
...state,
data: newData
}
P.S: If you want to get smarty pants, you can use XOR for your status update: ({...entry, status: entry.status ^ 1})
You are actually mutating the state. Try this...
import * as actionTypes from './actions';
const initialState = {
data: [
{id: 1, name: "accordion1", content: () => {}, status: 1},
{id: 2, name: "accordion2", content: () => {}, status: 0},
{id: 3, name: "accordion3", content: () => {}, status: 0},
]
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.ACTIVE_STATE:
return {
...state,
data: state.data.map((acdnObj) => {
return {
...acdnObj,
status: acdnObj.status === 1 ? 0 : 1,
}
}),
}
default:
return state;
}
}
export default reducer;

How to set a counter for duplicate values in React?

My code is basically a form with a text input and a submit button. Each time the user input data, my code adds it to an array and shows it under the form.
It is working fine; however, when I add duplicate values, it still adds it to the list. I want my code to count these duplicates and show them next to each input.
For example, if I input two "Hello" and one "Hi" I want my result to be like this:
2 Hello
1 Hi
Here is my code
import React from 'react';
import ShoppingItem from './ShoppingItem';
class ShoppingList extends React.Component {
constructor (props){
super(props);
this.state ={
shoppingCart: [],
newItem :'',
counter: 0 };
}
handleChange =(e) =>
{
this.setState ({newItem: e.target.value });
}
handleSubmit = (e) =>
{
e.preventDefault();
let newList;
let myItem ={
name: this.state.newItem,
id:Date.now()
}
if(!this.state.shoppingCart.includes(myItem.name))
{
newList = this.state.shoppingCart.concat(myItem);
}
if (this.state.newItem !=='')
{
this.setState(
{
shoppingCart: newList
}
);
}
this.state.newItem ="" ;
}
the rest of my code is like this:
render(){
return(
<div className = "App">
<form onSubmit = {this.handleSubmit}>
<h6>Add New Item</h6>
<input type = "text" value = {this.state.newItem} onChange ={this.handleChange}/>
<button type = "submit">Add to Shopping list</button>
</form>
<ul>
{this.state.shoppingCart.map(item =>(
<ShoppingItem item={item} key={item.id} />
)
)}
</ul>
</div>
);
}
}
export default ShoppingList;
Issues
this.state.shoppingCart is an array of objects, so this.state.shoppingCart.includes(myItem.name) will always return false as it won't find a value that is a string.
this.state.newItem = ""; is a state mutation
Solution
Check the newItem state first, if empty then return early
Search this.state.shoppingCart for the index of the first matching item by name property
If found then you want to map the cart to a new array and then also copy the item into a new object reference and update the quantity.
If not found then copy the array and append a new object to the end with an initial quantity 1 property.
Update the shopping cart and newItem state.
Code
handleSubmit = (e) => {
e.preventDefault();
if (!this.state.newItem) return;
let newList;
const itemIndex = this.state.shoppingCart.findIndex(
(item) => item.name === this.state.newItem
);
if (itemIndex !== -1) {
newList = this.state.shoppingCart.map((item, index) =>
index === itemIndex
? {
...item,
quantity: item.quantity + 1
}
: item
);
} else {
newList = [
...this.state.shoppingCart,
{
name: this.state.newItem,
id: Date.now(),
quantity: 1
}
];
}
this.setState({
shoppingCart: newList,
newItem: ""
});
};
Note: Remember to use item.name and item.quantity in your ShoppingItem component.
Replace your "handleSubmit" with below one and check
handleSubmit = (e) => {
e.preventDefault();
const { shoppingCart, newItem } = this.state;
const isInCart = shoppingCart.some(({ itemName }) => itemName === newItem);
let updatedCart = [];
let numberOfSameItem = 1;
if (!isInCart && newItem) {
updatedCart = [
...shoppingCart,
{
name: `${numberOfSameItem} ${newItem}`,
id: Date.now(),
itemName: newItem,
counter: numberOfSameItem
}
];
} else if (isInCart && newItem) {
updatedCart = shoppingCart.map((item) => {
const { itemName, counter } = item;
if (itemName === newItem) {
numberOfSameItem = counter + 1;
return {
...item,
name: `${numberOfSameItem} ${itemName}`,
itemName,
counter: numberOfSameItem
};
}
return item;
});
}
this.setState({
shoppingCart: updatedCart,
newItem: ""
});
};

Adding form data and rendering to the DOM React

I am creating warehouse management application where I enter data to the form in the form component. When submitting form I want to render entered data to the DOM, in the local storage: http://localhost:3000/products/. changeHandler works good, I receive data from input fields. However, addItemHandler does not work and it does not render anything. Could anybody help me, please.
import React, { Component, createContext } from "react";
import nextId from "react-id-generator";
const ProductContext = createContext();
class ProductProvider extends Component {
newId = nextId();
state = {
products: [
{ id: "", name: "", ean: "", type: "", weight: "", color: "", quantity: "", price: "", info: "", }, ],
detailProduct: "",
};
componentDidMount() {
this.setProducts();
}
setProducts = () => {
let products = [];
this.state.products.forEach((item) => {
const singleItem = { ...item };
products = [...products, singleItem];
});
this.setState(() => {
return { products: products };
});
};
getItem = (id) => {
const product = this.state.products.find((item) => item.id === id);
return product;
};
productDetailHandler = (id) => {
const product = this.getItem(id);
this.setState(() => {
return { detailProduct: product };
});
};
changeHandler = (event) => {
const value = event.target.value;
this.setState({
products: { ...this.state.products, [event.target.name]: value },
});
};
addItemHandler = ( event, name, ean, type, weight, color, quantity, price, info ) => {
event.preventDefault();
const products = [
...this.state.products,
{ name, ean, type, weight, color, quantity, price, info, id: this.newID },
];
this.setState({ products: products });
console.log(this.state);
};
render() {
return (
<ProductContext.Provider
value={{
...this.state,
productDetailHandler: this.productDetailHandler,
changeHandler: this.changeHandler,
addItemHandler: this.addItemHandler,
}}
>
{this.props.children}
</ProductContext.Provider>
);
}
}
const ProductConsumer = ProductContext.Consumer;
export { ProductProvider, ProductConsumer };
state = {
...
newProductItem: {}
};
// the `products`'s type is an Object Array
changeHandler = (event) => {
const value = event.target.value;
const {newProductItem} = this.state
this.setState({
newProductItem: { ...newProductItem, [event.target.name]: value}
});
}
addItemHandler = (event) => {
event.preventDefault();
const {newProductItem} = this.state
const products = [
...this.state.products,
{...newProductItem, id:this.newID}
];
this.setState({
products,
newProductItem: {}
});
};
==== in diff page ==
// form.jsx
state = {
...
newProductItem: {}
};
...
changeHandler = (event) => {
const value = event.target.value;
const { newProductItem } = this.state
this.setState({
newProductItem: { ...newProductItem, [event.target.name]: value}
});
}
...
render(){
...
const { addItemHandler} = this.props
const { newProductItem } = this.state
<Form submit={(ev)=> {
addItemHandler(ev, newProductItem);
this.setState({newProductItem:{}});
}>
<Input onChange={changeHandler} />
</Form>
}
...
<>
...
// main.jsx
addItemHandler = (event, newProductItem) => {
event.preventDefault();
const products = [
...this.state.products,
{...newProductItem, id:this.newID}
];
this.setState({
products
});
};

Redux increase quantity in a shopping cart

I am struggling with the function of my reducer to add an item and increase its quantity if already present in cart.
What my code does so far is adding another "quantity" with 1 instead of updating the quantity already present in my state.
Here's my code :
reducer :
import { ADD_TO_CART } from "../actions/types";
export default function(state = [], action) {
switch (action.type) {
case ADD_TO_CART:
if (state.findIndex(el => el.item.title === action.item.title) === -1) {
return [...state, { item: action.item, quantity: action.quantity + 1 }];
} else {
return [...state, { quantity: action.quantity + 1 }];
}
default:
return state;
}
}
action :
import { ADD_TO_CART } from "./types";
import axios from "axios";
export const addToCart = id => dispatch => {
axios
.get(`https://api.itbook.store/1.0/search/${id}`)
.then(items =>
items.map((item, quantity) =>
dispatch({
type: ADD_TO_CART,
item,
quantity
})
)
);
};
Thanks
You are finding the index (which is great) but not doing anything with it (which is not so great):
import { ADD_TO_CART } from "../actions/types";
export default function(state = [], action) {
switch (action.type) {
case ADD_TO_CART:
const index = state.findIndex(el => el.item.title === action.item.title);
if (index === -1) {
return [...state, { item: action.item, quantity: action.quantity + 1 }];
} else {
// Use map to create a new state object
return state.map((item, i) =>
index === i //Only modify the found index
? { ...item, quantity: item.quantity + action.quantity } //Add the required quantity to the current quantity (not too sure about this)
: item //Don't modify other items
);
}
default:
return state;
}
}
import { ADD_TO_CART } from "../actions/types";
export default function (state = [], action) {
switch (action.type) {
case ADD_TO_CART:
const index = state.findIndex(el => el.item.title === action.item.title);
if (index > -1) {
const newState = [...state];
newState[index] = { ...newState[index], quantity: action.quantity + 1 };
return newState;
} else {
return [...state, { ...action.item, quantity: action.quantity + 1 }];
}
default:
return state;
}
}

Edit a property in an array of objects in React based on an ID

I have an array of objects created in the new "Context API" like this ...
const reducer = (state, action) => {
switch (action.type) {
case "DELETE_CONTACT":
return {
...state,
contacts: state.contacts.filter(contact => {
return contact.id !== action.payload;
})
};
default:
return state;
}
};
export class Provider extends Component {
state = {
contacts: [
{
id: 1,
name: "John Doe",
email: "jhon.doe#site.com",
phone: "01027007024",
show: false
},
{
id: 2,
name: "Adam Smith",
email: "adam.smith#site.com",
phone: "01027007024",
show: false
},
{
id: 3,
name: "Mohammed Salah",
email: "mohammed.salah#site.com",
phone: "01027007024",
show: false
}
],
dispatch: action => {
this.setState(state => reducer(state, action));
}
};
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
);
}
}
I want to create an action in the "reducer" that allows me to edit each contact's "show" property based on its id that I will pass to the action as a payload, how can I do that?
To avoid array mutation and retain the element position while editing contact you can do this:
case "EDIT_CONTACT":
const { id, show } = action.payload;
const contact = { ...state.contacts.find(c => c.id === id), show };
return {
...state,
contacts: state.contacts.map(c => {return (c.id !== id) ? c : contact;})
};
You can find the contact, avoid mutation by using spread, set new value of show :
case "EDIT_CONTACT":
const { id, show } = action.payload; // Assume id and show are in action.payload
const contact = { ...state.contacts.find(c => c.id === id), show };
return {
...state,
contacts: [...state.contacts.filter(c => c.id !== id), contact]
};
If order matters:
const { id, show } = action.payload;
const contact = { ...state.contacts.find(c => c.id === id), show };
const index = state.contacts.findIndex(c => c.id === id);
return {
...state,
contacts = [ ...state.contacts.slice(0, index), contact, ...state.contacts.slice(index + 1)];
}

Categories

Resources