#node.js #reactjs #react-hooks #react-context
Вопрос:
У меня есть мини-приложение для покупок, которое использует корзину useState
. Теперь я хочу изменить состояние приложения, чтобы управлять useReducer
им и продолжать сохранять данные localStorage
.
У меня возникли проблемы с тем, чтобы понять, как выполнить рефакторинг, с участием многих движущихся частей. Как мне переработать внутреннюю логику addToCartHandler
, чтобы она вместо этого использовалась внутри ADD_TO_CART
корпуса? Оттуда, я полагаю, я смог бы выяснить закономерность для других случаев в cartReducer
. Спасибо.
Ответ №1:
Используйте контекстный API для управления состоянием корзины
Я бы начал с изоляции состояния вашей корзины и сохранения в локальном хранилище для поставщика контекста react. Контекст может предоставлять диспетчер состояния корзины и действий остальной части приложения, а также сохранять состояние в локальном хранилище при обновлении состояния с использованием эффекта. Это отделяет все управление состоянием от приложения, приложению нужно только использовать контекст для доступа к состоянию корзины и отправлять действия для его обновления.
import React, { createContext, useEffect, useReducer } from "react";
import { cartReducer, initializer } from "../cartReducer";
export const CartContext = createContext();
export const CartProvider = ({ children }) => {
const [cart, dispatch] = useReducer(cartReducer, [], initializer);
useEffect(() => {
localStorage.setItem("localCart", JSON.stringify(cart));
}, [cart]);
return (
<CartContext.Provider
value={{
cart,
dispatch
}}
>
{children}
</CartContext.Provider>
);
};
Оберните приложение CartProvider
в index.js
<CartProvider>
<App />
</CartProvider>
Завершите остальную часть приложения
В cartReducer
разделе доработка редуктора и экспорт функций инициализатора и создателей действий.
const initialState = [];
export const initializer = (initialValue = initialState) =>
JSON.parse(localStorage.getItem("localCart")) || initialValue;
export const cartReducer = (state, action) => {
switch (action.type) {
case "ADD_TO_CART":
return state.find((item) => item.name === action.item.name)
? state.map((item) =>
item.name === action.item.name
? {
...item,
quantity: item.quantity 1
}
: item
)
: [...state, { ...action.item, quantity: 1 }];
case "REMOVE_FROM_CART":
return state.filter((item) => item.name !== action.item.name);
case "DECREMENT_QUANTITY":
// if quantity is 1 remove from cart, otherwise decrement quantity
return state.find((item) => item.name === action.item.name)?.quantity ===
1
? state.filter((item) => item.name !== action.item.name)
: state.map((item) =>
item.name === action.item.name
? {
...item,
quantity: item.quantity - 1
}
: item
);
case "CLEAR_CART":
return initialState;
default:
return state;
}
};
export const addToCart = (item) => ({
type: "ADD_TO_CART",
item
});
export const decrementItemQuantity = (item) => ({
type: "DECREMENT_QUANTITY",
item
});
export const removeFromCart = (item) => ({
type: "REMOVE_FROM_CART",
item
});
export const clearCart = () => ({
type: "CLEAR_CART"
});
В Product.js
получить контекст корзины с помощью useContext
крючка и отправить addToCart
действие
import React, { useContext, useState } from "react";
import { CartContext } from "../CartProvider";
import { addToCart } from "../cartReducer";
const Item = () => {
const { dispatch } = useContext(CartContext);
...
const addToCartHandler = (product) => {
dispatch(addToCart(product));
};
...
return (
...
);
};
CartItem.js
получите и используйте контекст корзины для отправки действий по уменьшению количества или удалению товара.
import React, { useContext } from "react";
import { CartContext } from "../CartProvider";
import { decrementItemQuantity, removeFromCart } from "../cartReducer";
const CartItem = () => {
const { cart, dispatch } = useContext(CartContext);
const removeFromCartHandler = (itemToRemove) =>
dispatch(removeFromCart(itemToRemove));
const decrementQuantity = (item) => dispatch(decrementItemQuantity(item));
return (
<>
{cart.map((item, idx) => (
<div className="cartItem" key={idx}>
<h3>{item.name}</h3>
<h5>
Quantity: {item.quantity}{" "}
<span>
<button type="button" onClick={() => decrementQuantity(item)}>
<i>Decrement</i>
</button>
</span>
</h5>
<h5>Cost: {item.cost} </h5>
<button onClick={() => removeFromCartHandler(item)}>Remove</button>
</div>
))}
</>
);
};
App.js
получите состояние корзины и диспетчера через контекстный хук и обновите общее количество товаров и логику цен для учета количества товаров.
import { CartContext } from "./CartProvider";
import { clearCart } from "./cartReducer";
export default function App() {
const { cart, dispatch } = useContext(CartContext);
const clearCartHandler = () => {
dispatch(clearCart());
};
const { items, total } = cart.reduce(
({ items, total }, { cost, quantity }) => ({
items: items quantity,
total: total quantity * cost
}),
{ items: 0, total: 0 }
);
return (
<div className="App">
<h1>Emoji Store</h1>
<div className="products">
<Product />
</div>
<div className="cart">
<CartItem />
</div>
<h3>
Items in Cart: {items} | Total Cost: ${total.toFixed(2)}
</h3>
<button onClick={clearCartHandler}>Clear Cart</button>
</div>
);
}
Комментарии:
1. Вау, большое спасибо за то, что сделали приложение еще одним шагом вперед! Благодарен за вашу постоянную помощь.
2. @ln09nv2 Да, не за что. Я был примерно на 75% погружен в предложенные мной изменения, когда Захарий опубликовал свой ответ, и я хотел убедиться, что внес в таблицу что-то достаточно другое, чтобы на него стоило ответить.
3. Вместо этого я принял ваш ответ, потому что это более оптимальное решение и что вы помогали мне раньше!
Ответ №2:
Вот моя работа над этим. Я добавил все кейсы для картредуктора, потому что мне было весело с ним.
В случае, если вы хотите поработать над этим самостоятельно, вот первый случай с настройкой для использования локального хранилища для сохранения значений элементов.
Обзор того, что я делаю, таков: Используйте переключатель, чтобы настроить новое состояние в редукторе, а затем установите состояние localStorage в новое значение каждый раз, когда тележка изменяется с помощью эффекта.
Логика в продукте просто заменена простой отправкой действий. Поскольку логика вместо этого в редукторе. Вероятно, вы могли бы упростить логику в этом ADD_TO_CART
случае, но это решает все и неизменным образом. Использование чего-то вроде immer значительно упростило бы логику.
const storageKey = "localCart";
const cartReducer = (state, action) => {
switch (action.type) {
case "ADD_TO_CART": {
const product = action.payload;
let index = state.findIndex((item) => product.name === item.name);
if (index >= 0) {
const newState = [...state];
newState.splice(index, 1, {
...state[index],
quantity: state[index].quantity 1
});
return newState
} else {
return [...state, { ...product, quantity: 1 }];
}
}
default:
throw new Error();
}
};
Использование в App
компоненте:
const [cart, cartDispatch] = useReducer(
cartReducer,
[],
// So we only have to pull from localStorage one time - Less file IO
(initial) => JSON.parse(localStorage.getItem(storageKey)) || initial
);
useEffect(() => {
// This is a side-effect and belongs in an effect
localStorage.setItem(storageKey, JSON.stringify(cart));
}, [cart]);
Использование в Product
компоненте:
const addToCartHandler = (product) => {
dispatch({ type: "ADD_TO_CART", payload: product });
};
Полный рабочий код и коробка
Комментарии:
1. Большое вам спасибо за объяснение шагов рефакторинга! Я также ценю прокомментированный код, чтобы помочь моему пониманию.