使用 useReducer 持久化 localStorage

Posted

技术标签:

【中文标题】使用 useReducer 持久化 localStorage【英文标题】:Persist localStorage with useReducer 【发布时间】:2021-10-01 01:50:25 【问题描述】:

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

我很难弄清楚如何重构,因为涉及到许多移动的部分。如何重构 addToCartHandler 中的逻辑以在 ADD_TO_CART 案例中使用?从那里,我相信我能够找出cartReducer 中其他案例的模式。谢谢。

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

【问题讨论】:

【参考方案1】:

使用 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>

完成应用程序的其余部分

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

【讨论】:

哇,非常感谢您让应用程序更进一步!感谢您一直以来的帮助。 @ln09nv2 是的,不客气。当 Zacharey 发布他们的答案时,我对提议的更改进行了大约 75% 的更改,我想确保我带来了足够不同的东西,值得回答。 我接受了您的回答,因为它是更理想的解决方案,而且您以前帮助过我!【参考方案2】:

这是我的工作。我为 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

【讨论】:

非常感谢您解释重构的步骤!我也感谢注释代码以帮助我理解。

以上是关于使用 useReducer 持久化 localStorage的主要内容,如果未能解决你的问题,请参考以下文章

如何在 React 中使用 useReducer 更新嵌套对象?

P06:useReducer介绍与使用方法

使用带有 useReducer() 钩子的 React Context API 有啥优点和缺点?

在 React 中使用 useReducer 钩子过滤列表?

如何使用 useReducer 钩子测试组件?

React中useReducer的理解与使用