【问题标题】:useReducer: dispatch action, show state in other component and update state when action is dispatcheduseReducer:派发动作,在其他组件中显示状态,并在动作被派发时更新状态
【发布时间】:2022-01-14 16:43:49
【问题描述】:

我有一个我无法解决的问题。我正在构建一个电子商务反应应用程序并使用useReduceruseContext 进行状态管理。客户打开一个产品,选择项目数量,然后单击“添加到购物车”按钮,该按钮调度一个操作。这部分运行良好,问题开始了。我不知道如何在Navbar.js 组件中显示和更新购物车中的产品总数。它在路线更改后显示,但我希望它在单击“添加到购物车”按钮时更新。我试过 useEffect 但它不起作用。

初始状态是这样的

const initialState = [
  {
    productName: '',
    count: 0
  }
]

AddToCart.js 效果很好

import React, { useState, useContext } from 'react'
import { ItemCounterContext } from '../../App'

function AddToCart({ product }) {
  const itemCounter = useContext(ItemCounterContext)
  const [countItem, setCountItem] = useState(0)

  const changeCount = (e) => {
    if (e === '+') { setCountItem(countItem + 1) }
    if (e === '-' && countItem > 0) { setCountItem(countItem - 1) }
  }

  return (
    <div className='add margin-top-small'>
      <div
        className='add-counter'
        onClick={(e) => changeCount(e.target.innerText)}
        role='button'
      >
        -
      </div>

      <div className='add-counter'>{countItem}</div>

      <div
        className='add-counter'
        onClick={(e) => changeCount(e.target.innerText)}
        role='button'
      >
        +
      </div>
      <button
        className='add-btn btnOrange'
        onClick={() => itemCounter.dispatch({ type: 'addToCart', productName: product.name, count: countItem })}
      >
        Add to Cart
      </button>
    </div>
  )
}

export default AddToCart

Navbar.js 是我遇到问题的地方

import React, { useContext } from 'react'
import { Link, useLocation } from 'react-router-dom'
import NavList from './NavList'
import { StoreContext, ItemCounterContext } from '../../App'
import Logo from '../Logo/Logo'

function Navbar() {
  const store = useContext(StoreContext)
  const itemCounter = useContext(ItemCounterContext)
  const cartIcon = store[6].cart.desktop
  const location = useLocation()
  const path = location.pathname

  const itemsSum = itemCounter.state
    .map((item) => item.count)
    .reduce((prev, curr) => prev + curr, 0)

  const totalItemsInCart = (
    <span className='navbar__elements-sum'>
      {itemsSum}
    </span>
  )

  return (
    <div className={`navbar ${path === '/' ? 'navTransparent' : 'navBlack'}`}>
      <nav className='navbar__elements'>
        <Logo />
        <NavList />
        <Link className='link' to='/cart'>
          <img className='navbar__elements-cart' src={cartIcon} alt='AUDIOPHILE CART ICON' />
          {itemsSum > 0 ? totalItemsInCart : null}
        </Link>
      </nav>
    </div>
  )
}

export default Navbar

【问题讨论】:

  • AddToCartNavbar 似乎在使用不同的上下文。您可以将所有相关代码添加到您的问题中吗?你能分享这些上下文和减速器钩子等......吗?更新上下文值应该足以触发任何上下文使用者使用最新的上下文值重新呈现。
  • 我想我看到了你更大的问题......你正在记忆初始状态并且从不更新上下文值。这解释了为什么您的状态没有“更新”,或者更确切地说,为什么消费者看不到更新,以及为什么改变状态会泄漏它们。请参阅我的更新答案。如果您仍然存在问题,请尝试将您的代码分叉到 running 代码框中,我们可以实时检查和调试。
  • 您有机会查看更新的答案吗?
  • 是的,RTK 是对旧版 react-redux 的卓越改进。 useMemo 只是简单地记忆一个值,依赖数组是您想要重新记忆一个值的引用。这通常是为了提供对值的稳定引用,而不是触发不必要的重新渲染。在您的情况下,您使用了一个空的依赖数组,因此记忆值是在初始渲染时计算的,并且从未更新。 RTK 很棒,因为它允许您编写可变的 reducer,并在后台处理不可变性问题,结果是您的 reducer 代码更具逻辑性。
  • 现在我明白了。我将console.logs 放在reducer 函数中的任何地方,以找出问题所在,并且效果很好。当 console.logs 返回更新状态时,我感到震惊。确实认为减速器功能是问题所在。状态更新正常,但 useMemo 出现问题。 RTK 比传统的 redux 简单得多,也许我错了,但是 useContext 和 useReducer 似乎比 RTK 更简单。非常感谢!这很有帮助。

标签: javascript reactjs react-hooks react-state-management use-reducer


【解决方案1】:

问题出在您的 reducer 中,尤其是在您将先前的状态分配给 newState 以进行突变并返回更新状态的情况下。在 JavaScript 中,非原始对象是通过地址而不是值来引用的。由于您的数组 initialState 恰好是非原始数组,因此当您将非原始数组分配给新变量时,该变量仅指向内存中的现有数组,不会创建新副本。而且,只有在重建状态时才会触发/广播更新(这就是 React 理解存在更新的方式)而不是软突变。当您变异并返回newState 时,您基本上是在变异现有的state 而不会导致它重建。一个快速的解决方法是将您的state 复制到newState 而不仅仅是分配它。这可以使用扩展运算符 (...) 来完成。
在您的 reducer 函数中,更改:

const newState = state

const newState = [...state]

你的 reducer 函数应该如下所示:

export const reducer = (state, action) => {
  // returns -1 if product doesn't exist
  const indexOfProductInCart = state.findIndex((item) => item.productName === action.productName)
  const newState = [...state] //Deep-copying the previous state

  switch (action.type) {
    case 'increment': {
      if (indexOfProductInCart === -1) {
        newState[state.length] = { productName: action.productName, count: state.count + 1 }
        return newState
      }
      newState[indexOfProductInCart] = { productName: action.productName, count: state.count + 1 }
      return newState
    }
    case 'decrement': {
      if (indexOfProductInCart === -1) {
        newState[state.length] = { productName: action.productName, count: state.count - 1 }
        return newState
      }
      newState[indexOfProductInCart] = { productName: action.productName, count: state.count - 1 }
      return newState
    }
    case 'addToCart': {
      if (indexOfProductInCart === -1) {
        newState[state.length] = { productName: action.productName, count: action.count }
        return newState
      }
      newState[indexOfProductInCart] = { productName: action.productName, count: action.count }
      return newState
    }
    case 'remove': return state.splice(indexOfProductInCart, 1)
    default: return state
  }
}

【讨论】:

  • const newState = [...state] 只是一个浅拷贝,仅供参考。 ?
  • 但是,它并没有指向内存中的同一个位置,对吧?可以?支持这一点的一个简单证据是检查它们的相等性。
  • 是浅拷贝,数组是一个新的引用,是的,但是所有的元素都是按引用复制的,也就是说它们仍然引用原始数组中的所有元素。换句话说,它不是 深拷贝
【解决方案2】:

看来你正在改变你的 reducer 函数中的 state 对象。您首先使用const newState = state 保存对状态的引用,然后使用每个newState[state.length] = ..... 改变该引用,然后使用return newState 为下一个状态返回相同的状态引用。下一个状态对象绝不是 new 对象引用。

考虑以下使用各种数组方法对state 数组进行操作并返回 数组引用:

export const reducer = (state, action) => {
  // returns -1 if product doesn't exist
  const indexOfProductInCart = state.findIndex(
    (item) => item.productName === action.productName
  );

  const newState = state.slice(); // <-- create new array reference

  switch (action.type) {
    case 'increment': {
      if (indexOfProductInCart === -1) {
        // Not in cart, append with initial count of 1
        return newState.concat({
          productName: action.productName,
          count: 1,
        });
      }
      // In cart, increment count by 1
      newState[indexOfProductInCart] = {
        ...newState[indexOfProductInCart]
        count: newState[indexOfProductInCart].count + 1,
      }
      return newState;
    }

    case 'decrement': {
      if (indexOfProductInCart === -1) {
        // Not in cart, append with initial count of 1
        return newState.concat({
          productName: action.productName,
          count: 1,
        });
      }
      // In cart, decrement count by 1, to minimum of 1, then remove
      if (newState[indexOfProductInCart].count === 1) {
        return state.filter((item, index) => index !== indexOfProductInCart);
      }
      newState[indexOfProductInCart] = {
        ...newState[indexOfProductInCart]
        count: Math.max(0, newState[indexOfProductInCart].count - 1),
      }
      return newState;
    }

    case 'addToCart': {
      if (indexOfProductInCart === -1) {
        // Not in cart, append with initial action count
        return newState.concat({
          productName: action.productName,
          count: action.count,
        });
      }
      // Already in cart, increment count by 1
      newState[indexOfProductInCart] = {
        ...newState[indexOfProductInCart]
        count: newState[indexOfProductInCart].count + 1,
      }
      return newState;
    }

    case 'remove':
      return state.filter((item, index) => index !== indexOfProductInCart);

    default: return state
  }
}

Navbar 中的itemsSum 现在应该会从上下文中看到状态更新。

const itemsSum = itemCounter.state
  .map((item) => item.count)
  .reduce((prev, curr) => prev + curr, 0);

您似乎还记住了 useMemo 钩子中的 state 值和一个空的依赖数组。这意味着传递给StoreContext.Providercounter 值永远不会更新。

function App() {
  const initialState = [{ productName: '', count: 0 }];
  const [state, dispatch] = useReducer(reducer, initialState);

  const counter = useMemo(() => ({ state, dispatch }), []); // <-- memoized the initial state value!!!

  return (
    <div className='app'>
      <StoreContext.Provider value={store}> // <-- passing memoized state
        ...
      </StoreContext.Provider>
    </div>
  )
}

state添加到依赖数组中

const counter = useMemo(() => ({ state, dispatch }), [state]);

或者根本不记住它并将statedispatch传递给上下文value

<StoreContext.Provider value={{ state, dispatch }}>
  ...
</StoreContext.Provider>

【讨论】:

    【解决方案3】:

    嗯,ItemCounterContext对于这个问题很重要,忽略StoreContext,它是用于图像的……这里有一个reducer函数。

    export const reducer = (state, action) => {
      // returns -1 if product doesn't exist
      const indexOfProductInCart = state.findIndex((item) => item.productName === action.productName)
      const newState = state
    
      switch (action.type) {
        case 'increment': {
          if (indexOfProductInCart === -1) {
            newState[state.length] = { productName: action.productName, count: state.count + 1 }
            return newState
          }
          newState[indexOfProductInCart] = { productName: action.productName, count: state.count + 1 }
          return newState
        }
        case 'decrement': {
          if (indexOfProductInCart === -1) {
            newState[state.length] = { productName: action.productName, count: state.count - 1 }
            return newState
          }
          newState[indexOfProductInCart] = { productName: action.productName, count: state.count - 1 }
          return newState
        }
        case 'addToCart': {
          if (indexOfProductInCart === -1) {
            newState[state.length] = { productName: action.productName, count: action.count }
            return newState
          }
          newState[indexOfProductInCart] = { productName: action.productName, count: action.count }
          return newState
        }
        case 'remove': return state.splice(indexOfProductInCart, 1)
        default: return state
      }
    }
    

    这里是 App.js,我在其中与其他组件共享状态

    import React, { createContext, useMemo, useReducer } from 'react'
    import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
    import Navbar from './components/Navbar/Navbar'
    import Homepage from './pages/Homepage/Homepage'
    import Footer from './components/Footer/Footer'
    import ErrorPage from './pages/ErrorPage/ErrorPage'
    import SelectedCategory from './pages/SelectedCategory/SelectedCategory'
    import SingleProduct from './pages/SingleProduct/SingleProduct'
    import ScrollToTop from './services/ScrollToTop'
    import store from './services/data.json'
    import { reducer } from './services/ItemCounter'
    import './scss/main.scss'
    
    export const StoreContext = createContext(store)
    export const ItemCounterContext = createContext()
    
    function App() {
      const initialState = [{ productName: '', count: 0 }]
      const [state, dispatch] = useReducer(reducer, initialState)
      const counter = useMemo(() => ({ state, dispatch }), [])
    
      return (
        <div className='app'>
          <StoreContext.Provider value={store}>
            <ItemCounterContext.Provider value={counter}>
              <Router>
                <ScrollToTop />
                <Navbar />
                <Routes>
                  <Route path='/' element={<Homepage />} />
                  <Route path='/:selectedCategory' element={<SelectedCategory />} />
                  <Route path='/:selectedCategory/:singleProduct' element={<SingleProduct />} />
                  <Route path='*' element={<ErrorPage />} />
                </Routes>
                <Footer />
              </Router>
            </ItemCounterContext.Provider>
          </StoreContext.Provider>
        </div>
      )
    }
    
    export default App
    

    【讨论】:

    • 由于我不认为您试图在这里回答您自己的问题而只是添加信息,您可能希望将这些详细信息移到您的问题中并删除此答案。
    【解决方案4】:

    我确切地知道你在说什么,但 reducer 的问题是只有可变方法适用于状态。 .slice()、.concat() 甚至扩展运算符 [...state] 等不可变方法不起作用,我不知道为什么 :( 我尝试了两个答案,但 dispatch(action) 并没有改变状态。也许初始状态是问题,我会试着把它说成这样

    初始状态 = { 购物车:[ 产品名称: '', 计数:0 ] }

    【讨论】:

    • 同样,这应该是对您的帖子的评论或对答案的回应。您应该永远在 React 中改变状态,无论您在类组件、redux 等中是否使用 useReduceruseStatestate 都没有关系。 ...您应该始终使用不可变的更新模式。 IMO 你的initialState 应该是一个空数组,即const initialState = [],因为你还没有将任何物品添加到购物车中。
    猜你喜欢
    • 2020-11-09
    • 1970-01-01
    • 2021-12-10
    • 1970-01-01
    • 2020-06-16
    • 2022-01-05
    • 2016-11-01
    • 2021-11-22
    • 1970-01-01
    相关资源
    最近更新 更多