Сохраняйте локальный склад с помощью useReducer

#node.js #reactjs #react-hooks #react-context

Вопрос:

У меня есть мини-приложение для покупок, которое использует корзину useState . Теперь я хочу изменить состояние приложения, чтобы управлять useReducer им и продолжать сохранять данные localStorage .

У меня возникли проблемы с тем, чтобы понять, как выполнить рефакторинг, с участием многих движущихся частей. Как мне переработать внутреннюю логику addToCartHandler , чтобы она вместо этого использовалась внутри ADD_TO_CART корпуса? Оттуда, я полагаю, я смог бы выяснить закономерность для других случаев в cartReducer . Спасибо.

https://codesandbox.io/s/goofy-water-pb903?file=/src/App.js

Ответ №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>
 

Изменить persist-локальный накопитель-с помощью usereducer

Завершите остальную часть приложения

В 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. Большое вам спасибо за объяснение шагов рефакторинга! Я также ценю прокомментированный код, чтобы помочь моему пониманию.