使用 useReducer 保留 localStorage

问题描述 投票:0回答:3

我有一个使用

useState
的迷你购物车应用程序。我现在想重构应用程序的状态以由
useReducer
管理并继续使用
localStorage
持久化数据。

我在弄清楚如何重构时遇到了麻烦,因为涉及到许多移动的部分。我如何着手重构

addToCartHandler
中的逻辑以代替在
ADD_TO_CART
案例中使用?从那里,我相信我能够找出
cartReducer
中其他案例的模式。谢谢。

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

reactjs react-hooks local-storage use-reducer
3个回答
26
投票

使用 Context API 管理购物车状态

我首先将您的购物车状态和本地存储的持久性隔离到反应上下文提供者。上下文可以为应用程序的其余部分提供购物车状态和操作调度程序,并在使用效果更新状态时将状态持久保存到 localStorage。这将所有状态管理与应用程序分离,应用程序只需要使用上下文来访问购物车状态并调度操作来更新它。

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>
  );
};

在 index.js 的

CartProvider
中包装应用程序

<CartProvider>
  <App />
</CartProvider>

Edit persist-localstorage-with-usereducer

完善应用程序的其余部分

cartReducer
精化reducer,并导出初始化函数和action creators。

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>
  );
}

6
投票

这是我的工作。我为 cartReducer 添加了所有案例,因为我玩得很开心。

如果您想自己完成它,这是第一个使用 localStorage 来保存项目值的设置案例。

我所做的概述是:使用 switch case 在 reducer 中设置新状态,然后每次购物车通过效果更改时将 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 });
  };

全面工作CodeSandbox


0
投票

这是我创建的一个简单的 useSessionStorage 挂钩:

import { Dispatch, Reducer, useCallback, useEffect, useReducer } from 'react';

const initializer =
  (key: string) =>
  <T>(initial: T) => {
    const stored = sessionStorage.getItem(key);
    if (!stored) return initial;
    return JSON.parse(stored);
  };

export const useSessionReducer = <T extends object, A>(
  reducer: Reducer<T, A>,
  initialState: T,
  key: string,
): [T, Dispatch<A>, VoidFunction] => {
  const [state, dispatch] = useReducer(reducer, initialState, initializer(key));
  const clearValue = useCallback(() => sessionStorage.removeItem(key), [key]);

  useEffect(() => {
    sessionStorage.setItem(key, JSON.stringify(state));
  }, [state]);

  return [state, dispatch, clearValue];
};

这就是您的使用方式:

const [state, dispatch, clearSaved] = useSessionReducer(cartReducer, initialState, 'cart');

附言将 sessionStorage 与 localStorage 交换不会改变挂钩的工作方式。

© www.soinside.com 2019 - 2024. All rights reserved.