Я только начал учиться реагировать и столкнулся с этой проблемой, когда у меня есть две кнопки (кнопка «Добавить» в элемент меню и кнопка «Добавить в корзину»), каждая из которых определена в разных компонентах. Я определил функциональность и добавил логику с помощью реквизитов для кнопки «Добавить» в элементе меню, который, когда пользователь нажимает, обновляет количество, cartCount и добавляет детали и количество товара в корзину. Теперь, когда я нажимаю кнопку «Добавить» в корзине, мне нужна аналогичная функциональность, например, она должна обновлять количество в корзине, количество в пункте меню и количество корзин. Я знаю, что должен быть какой-то способ, чтобы не повторять всю логику снова. Я знаю, что это долго. Заранее спасибо за ответ!!!
App.js
import react, { useState, useEffect } from "react";
import Header from "./components/Header";
import LandingPage from "./components/LandingPage";
import MenuItems from "./components/menuItems";
import Cart from "./components/Cart";
import ItemContext from "./store/item-context";
function App() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(() => {
return items.reduce((acc, eachItem) => {
return eachItem.quantity + acc;
}, 0)
})
}, [items])
const [cartBool, setCartBool] = useState(false);
function AddedItem(item) {
const foundIndex = items.findIndex(eachItem => {
return eachItem.title === item.title;
})
if (foundIndex !== -1) {
setItems(prev => {
prev[foundIndex].quantity = item.quantity;
return [...prev];
})
}
else {
setItems(prev => {
return [...prev, item]
})
}
}
function handleCartClick() {
setCartBool(true);
}
function handleCloseClick() {
setCartBool(false);
}
return (
<react.Fragment>
<ItemContext.Provider value = {{
items: items
}}>
{cartBool &&
<Cart onCloseClick = {handleCloseClick} />}
<div className = "parent-container">
<Header cartCount = {total} onCartClick = {handleCartClick} />
<LandingPage />
<MenuItems onAddItem = {AddedItem} />
</div>
</ItemContext.Provider>
</react.Fragment>
);
}
export default App;
Menu-items.js
import react from "react";
import MenuItem from "./menuItem";
import MenuContent from "./menuContent";
function MenuItems(props) {
function handleItems(item){
props.onAddItem(item);
}
return (
<div className = "menu">
{MenuContent.map(eachItem =>{
return <MenuItem title = {eachItem.title} description = {eachItem.description} price = {eachItem.price} key = {eachItem.key} onAdd = {handleItems}/>
})}
</div>
);
}
export default MenuItems;
Меню-item.js
import react , { useState } from "react";
function MenuItem(props) {
const [item, setItem] = useState({
title: "",
quantity: 0,
price: ""
});
function handleClick(){
setItem(prev =>{
return {
title: props.title,
quantity: prev.quantity + 1,
price: props.price
}
})
}
function handleSubmit(event){
event.preventDefault();
props.onAdd(item);
}
return (
<div className = "menu-item">
<div className = "menu-content">
<h3>{props.title}</h3>
<p>{props.description}</p>
<h4>{props.price}</h4>
</div>
<form onSubmit = {handleSubmit} className = "add-items">
<label htmlFor = "Amount">Amount</label>
<input onChange = {() => {}} type = "number" name = "Amount" value = {item.quantity}/>
<button onClick = {handleClick} type = "submit" className = "btn btn-lg">Add</button>
</form>
</div>
);
}
export default MenuItem;`
Cart.js
import react, { useContext } from "react";
import CartItem from "./cartItem";
import ItemContext from "../store/item-context";
function Cart(props) {
const ctx = useContext(ItemContext);
function handleCloseClick(){
props.onCloseClick();
}
return (
<div className = "cart-modal">
<div className = "card">
{ctx.items.map((eachItem, index) =>{
return <CartItem title = {eachItem.title} price = {eachItem.price} quantity = {eachItem.quantity} key = {index} onAdd = {props.onAddItem} onRemove = {props.RemoveItem}/>
})}
<footer>
<button className = "btn btn-lg" onClick = {handleCloseClick}>Close</button>
<button className = "btn btn-lg">Order</button>
</footer>
</div>
</div>
);
}export default Cart;
cartItem.js
import react, { useState } from "react";
function CartItem(props) {
const [item, setItem] = useState({
title: props.title,
price: props.price,
quantity: props.quantity
})
function handlePlusClick(){
setItem(prev =>{
prev.quantity = prev.quantity + 1
return prev
})
props.onAdd(item);
}
function handleMinusClick(){
var updatedQuantity;
setItem(prev =>{
prev.quantity = prev.quantity -1
updatedQuantity = prev.quantity
return prev;
})
if (updatedQuantity > 0){
props.onAdd(item);
}
else{
props.onRemove(item);
}
}
return (
<div className = "cart-item">
<div className = "cart-content">
<h1>{props.title}</h1>
<p>{props.price}
<span> X {props.quantity}</span>
</p>
</div>
<div className = "button-controls">
<button onClick = {handleMinusClick}>-</button>
<button onClick = {handlePlusClick}>+</button>
</div>
</div>
);
}export default CartItem;
Я попытался создать новый объект элемента, когда пользователь нажал кнопку + в cartItem и отправил его в функцию AddedItem в App.js, и он работает (хотя это не лучшая практика) !!! Но он также обновляет item.quantity в компоненте menuItem. он работает, как и ожидалось... Но я понятия не имею, почему он возвращается и обновляет количество элементов меню. это из-за useContext, и я обернул его вокруг всех компонентов, которые я визуализирую??
🤔 А знаете ли вы, что...
С JavaScript можно создавать интерактивные формы и проверять введенные пользователем данные.
Ваш пример все еще немного сложно воспроизвести и воспроизвести, поскольку мы не видим MenuContent
, а использование useContext
сбивает с толку.
Но похоже, что и ваше меню, и корзина используют одно и то же состояние items
или, по крайней мере, что-то в этом роде происходит.
Ваш код демонстрирует управление состоянием, но я думаю, вам нужно сделать шаг назад и подумать о том, какие части вашего приложения должны иметь состояние и какие стратегии необходимы. Вам не нужно useContext
, но я полагаю, что это возможность проиллюстрировать различия и преимущества.
Сейчас я предполагаю, что ваши пункты меню представляют собой список пунктов, которые на самом деле не меняются. Для вашей корзины потребуется некоторое состояние, так как вам нужно отслеживать товары вместе с их количеством и использовать эту информацию для расчета общей суммы корзины.
Где нам нужно обновить или получить доступ к нашему состоянию корзины?
MenuItem
- В нашем пункте меню есть кнопка Add
, которая должна обновить состояние корзины с новым количеством. Здесь нам не нужны элементы корзины, но нам нужно обработать логику обновления нашей корзины.
Cart
- Наша корзина должна получить доступ к состоянию корзины, чтобы а) показать элементы корзины и б) увеличить или уменьшить количество определенных элементов (+ и -).
Вы можете сделать это с помощью просверливания, используя те же стратегии, которые использовались в вашем коде до сих пор (которыми вы поделились), ИЛИ вы можете использовать useContext
.
Чтобы продемонстрировать разницу, ниже приведено более полное решение с useContext
. Вся логика управления состоянием корзины встроена в контекст нашей корзины, и наш провайдер позволяет частям нашего приложения получить к ней доступ, не слишком полагаясь на реквизиты.
useContext
(нажмите, чтобы просмотреть)https://codesandbox.io/s/update-cart-example-use-context-4glul7
import "./styles.css";
import React, { useState, createContext, useContext, useReducer } from "react";
const CartContext = createContext();
const initialCartState = { cartItems: [], totalCost: 0, totalQuantity: 0 };
const actions = {
INCREMENT_ITEM: "INCREMENT_ITEM",
DECREMENT_ITEM: "DECREMENT_ITEM",
UPDATE_QUANTITY: "UPDATE_QUANTITY"
};
const reducer = (state, action) => {
const existingCartItem = state.cartItems.findIndex((item) => {
return item.id === action.itemToUpdate.id;
});
switch (action.type) {
case actions.INCREMENT_ITEM:
return {
cartItems: state.cartItems.map((item) =>
item.id === action.itemToUpdate.id
? {
...item,
quantity: item.quantity + 1
}
: item
),
totalQuantity: state.totalQuantity + 1,
totalCost: state.totalCost + action.itemToUpdate.price
};
case actions.DECREMENT_ITEM:
return {
cartItems: state.cartItems.map((item) =>
item.id === action.itemToUpdate.id
? {
...item,
quantity: item.quantity - 1
}
: item
),
totalQuantity: state.totalQuantity - 1,
totalCost: state.totalCost - action.itemToUpdate.price
};
case actions.UPDATE_QUANTITY:
return {
cartItems:
existingCartItem !== -1
? state.cartItems.map((item) =>
item.id === action.itemToUpdate.id
? {
...item,
quantity: item.quantity + action.itemToUpdate.quantity
}
: item
)
: [...state.cartItems, action.itemToUpdate],
totalQuantity: state.totalQuantity + action.itemToUpdate.quantity,
totalCost:
state.totalCost +
action.itemToUpdate.quantity * action.itemToUpdate.price
};
default:
return state;
}
};
const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialCartState);
const value = {
cartItems: state.cartItems,
totalQuantity: state.totalQuantity,
totalCost: state.totalCost,
incrementItem: (itemToUpdate) => {
dispatch({ type: actions.INCREMENT_ITEM, itemToUpdate });
},
decrementItem: (itemToUpdate) => {
dispatch({ type: actions.DECREMENT_ITEM, itemToUpdate });
},
updateQuantity: (itemToUpdate) => {
dispatch({ type: actions.UPDATE_QUANTITY, itemToUpdate });
}
};
return <CartContext.Provider value = {value}>{children}</CartContext.Provider>;
};
export default function App() {
return (
<CartProvider>
<MenuItems />
<Cart />
</CartProvider>
);
}
const menuItems = [
{ title: "item 1", description: "description 1", price: 10, id: "1" },
{ title: "item 2", description: "description 2", price: 20, id: "2" },
{ title: "item 3", description: "description 3", price: 30, id: "3" }
];
function MenuItems(props) {
return (
<div className = "menu">
{menuItems.map((item) => {
return (
<MenuItem
title = {item.title}
description = {item.description}
price = {item.price}
key = {item.id}
// added this as prop
id = {item.id}
/>
);
})}
</div>
);
}
function MenuItem(props) {
const { updateQuantity } = useContext(CartContext);
const [item, setItem] = useState({
title: props.title,
quantity: 0,
price: props.price,
// included a unique item id here
id: props.id
});
// Don't need this anymore...
// function handleClick(e) {
// ...
// }
// update quantity as we type by getting as state...
function changeQuantity(e) {
e.preventDefault();
setItem((prev) => {
return {
...prev,
quantity: Number(e.target.value)
};
});
}
function handleSubmit(e, item) {
e.preventDefault();
updateQuantity(item);
}
return (
<div className = "menu-item">
<div className = "menu-content">
<h3>{props.title}</h3>
<p>{props.description}</p>
<h4>Price: ${props.price}</h4>
</div>
<form onSubmit = {(e) => handleSubmit(e, item)} className = "add-items">
<label htmlFor = "Amount">Amount</label>
<input
onChange = {changeQuantity}
type = "number"
name = "Amount"
value = {item.quantity}
/>
{/* No need for onClick on button, onSubmit already handles it */}
<button type = "submit" className = "btn btn-lg">
Add
</button>
</form>
</div>
);
}
function Cart() {
const {
cartItems,
totalQuantity,
totalCost,
incrementItem,
decrementItem
} = useContext(CartContext);
return (
<div>
<h2>Cart</h2>
<h3>Items:</h3>
{cartItems.length > 0 &&
cartItems.map(
(item) =>
item.quantity > 0 && (
<div key = {item.id}>
{item.title}
<br />
<button onClick = {() => decrementItem(item)}> - </button>{" "}
{item.quantity}{" "}
<button onClick = {() => incrementItem(item)}> + </button>
</div>
)
)}
<h3>Total Items: {totalQuantity}</h3>
<h3>Total Cost: {`$${Number(totalCost).toFixed(2)}`}</h3>
</div>
);
}
Похоже, вы хотели, чтобы корзина обновлялась всякий раз, когда вы нажимали Add
в MenuItem
.
onClick
и onSubmit
Это было частью вашей проблемы. В MenuItem
вы использовали форму и имели onClick
на кнопке отправки формы. Поскольку у вашей кнопки есть type = "submit"
, она вызовет событие отправки вместе с обработчиком onSubmit
. Мы можем просто использовать onSubmit
в качестве нашего обработчика и удалить onClick
с кнопки.
Я упростил MenuItem
, чтобы обновить и прочитать значение количества из состояния. Затем при добавлении товара мы просто передаем товар (поскольку он уже имеет актуальное количество).
Ваша логика была в основном там. Я поставил каждому продукту id
, чтобы упростить отслеживание всех сверлений реквизита по сравнению с использованием title
или key
, так как мне было немного легче обернуть голову. Надеюсь, изменения и комментарии имеют смысл.
https://codesandbox.io/s/update-cart-example-veic1h
import "./styles.css";
import React, { useState, createContext, useContext, useEffect } from "react";
const CartContext = createContext();
export default function App() {
const [cartItems, setCartItems] = useState([]);
const [totalQuantity, setTotalQuantity] = useState(0);
const [totalCost, setTotalCost] = useState(0);
useEffect(() => {
setTotalQuantity(() => {
return cartItems.reduce((acc, item) => {
return item.quantity + acc;
}, 0);
});
setTotalCost(() => {
return cartItems.reduce((acc, item) => {
return item.quantity * item.price + acc;
}, 0);
});
}, [cartItems]);
function addItemToCart(newItem) {
const existingCartItem = cartItems.findIndex((item) => {
return item.id === newItem.id;
});
setCartItems((prevItems) => {
return existingCartItem !== -1
? prevItems.map((prevItem) =>
prevItem.id === newItem.id
? {
...prevItem,
quantity: prevItem.quantity + newItem.quantity
}
: prevItem
)
: [...prevItems, newItem];
});
// the above is similar to what you have below,
// but good practice not to mutate state directly
// in case of incrementing item already found in cart...
// if (foundIndex !== -1) {
// setCartItems((prev) => {
// prev[foundIndex].quantity = item.quantity;
// return [...prev];
// });
// } else {
// setCartItems((prev) => {
// return [...prev, item];
// });
// }
}
return (
<CartContext.Provider value = {{ cartItems, totalQuantity, totalCost }}>
<div className = "parent-container">
<MenuItems onAddItem = {addItemToCart} />
<Cart />
</div>
</CartContext.Provider>
);
}
const menuItems = [
{ title: "item 1", description: "description 1", price: 10, id: "1" },
{ title: "item 2", description: "description 2", price: 20, id: "2" },
{ title: "item 3", description: "description 3", price: 30, id: "3" }
];
function MenuItems(props) {
function handleItems(item) {
props.onAddItem(item);
}
return (
<div className = "menu">
{menuItems.map((item) => {
return (
<MenuItem
title = {item.title}
description = {item.description}
price = {item.price}
key = {item.id}
// added this as prop
id = {item.id}
onAdd = {handleItems}
/>
);
})}
</div>
);
}
function MenuItem(props) {
const [item, setItem] = useState({
title: props.title,
quantity: 0,
price: props.price,
// included a unique item id here
id: props.id
});
// Don't need this anymore...
// function handleClick(e) {
// ...
// }
// update quantity as we type by getting as state...
function changeQuantity(e) {
e.preventDefault();
setItem((prev) => {
return {
...prev,
quantity: Number(e.target.value)
};
});
}
function handleSubmit(event) {
event.preventDefault();
props.onAdd(item);
}
return (
<div className = "menu-item">
<div className = "menu-content">
<h3>{props.title}</h3>
<p>{props.description}</p>
<h4>Price: ${props.price}</h4>
</div>
<form onSubmit = {handleSubmit} className = "add-items">
<label htmlFor = "Amount">Amount</label>
<input
onChange = {changeQuantity}
type = "number"
name = "Amount"
value = {item.quantity}
/>
{/* No need for onClick on button, onSubmit already handles it */}
<button type = "submit" className = "btn btn-lg">
Add
</button>
</form>
</div>
);
}
function Cart() {
const cart = useContext(CartContext);
const { cartItems, totalQuantity, totalCost } = cart;
return (
<div>
<h2>Cart</h2>
<h3>Items:</h3>
{cartItems.length > 0 &&
cartItems.map(
(item) =>
item.quantity > 0 && (
<div key = {item.id}>
{item.title} - quantity: {item.quantity}
</div>
)
)}
<h3>Total Items: {totalQuantity}</h3>
<h3>Total Cost: {`$${Number(totalCost).toFixed(2)}`}</h3>
</div>
);
}