useReducer:调度动作,在其他组件中显示状态并在调度动作时更新状态
useReducer: dispatch action, show state in other component and update state when action is dispatched
我有一个我无法解决的问题。我正在构建一个电子商务反应应用程序并使用 useReducer
和 useContext
进行状态管理。客户打开产品,挑选商品数量,然后单击“添加到购物车”按钮以发送操作。这部分工作正常,问题开始了。我不知道如何在 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
嗯,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
您似乎正在改变 reducer 函数中的 state
对象。您首先使用 const newState = state
保存对状态的引用,然后用每个 newState[state.length] = .....
改变该引用,然后 return 对下一个状态的相同状态引用 return newState
。下一个状态对象永远不是 new 对象引用。
考虑以下使用各种数组方法对 state
数组和 return new 进行操作数组引用:
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
}
}
itemsSum
in Navbar
现在应该可以从上下文中看到状态更新。
const itemsSum = itemCounter.state
.map((item) => item.count)
.reduce((prev, curr) => prev + curr, 0);
您似乎还使用空依赖数组在 useMemo
挂钩中记住了 state
值。这意味着传递给 StoreContext.Provider
的 counter
值永远不会更新。
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]);
或者根本不记忆它并将 state
和 dispatch
传递给上下文 value
<StoreContext.Provider value={{ state, dispatch }}>
...
</StoreContext.Provider>
问题出在你的 reducer 上,特别是你将之前的状态分配给 newState
以进行突变和 return 更新后的状态。在 JavaScript 中,非基本类型是通过地址而不是值来引用的。由于你的 initialState
这是一个数组恰好是一个非原始的,所以当你将一个非原始的分配给一个新的变量时,这个变量只指向内存中现有的数组并且不会创建新的副本。并且,在 React 中,仅当状态被重建时(这就是 React 理解存在更新的方式)而不是软变异时,更新才 triggered/broadcasted 。当你改变 return 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
}
}
我很清楚你在说什么,但 reducer 中的问题是只有可变方法才适用于状态。像 .slice()、.concat() 甚至扩展运算符 [...state] 这样的不可变方法不起作用,我不知道为什么 :(
我尝试了这两个答案,但 dispatch(action) 并没有改变状态。也许初始状态是问题所在,我会尽量把它写成
初始状态={
购物车:[
产品名称: '',
计数:0
]
}
我有一个我无法解决的问题。我正在构建一个电子商务反应应用程序并使用 useReducer
和 useContext
进行状态管理。客户打开产品,挑选商品数量,然后单击“添加到购物车”按钮以发送操作。这部分工作正常,问题开始了。我不知道如何在 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
嗯,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
您似乎正在改变 reducer 函数中的 state
对象。您首先使用 const newState = state
保存对状态的引用,然后用每个 newState[state.length] = .....
改变该引用,然后 return 对下一个状态的相同状态引用 return newState
。下一个状态对象永远不是 new 对象引用。
考虑以下使用各种数组方法对 state
数组和 return new 进行操作数组引用:
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
}
}
itemsSum
in Navbar
现在应该可以从上下文中看到状态更新。
const itemsSum = itemCounter.state
.map((item) => item.count)
.reduce((prev, curr) => prev + curr, 0);
您似乎还使用空依赖数组在 useMemo
挂钩中记住了 state
值。这意味着传递给 StoreContext.Provider
的 counter
值永远不会更新。
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]);
或者根本不记忆它并将 state
和 dispatch
传递给上下文 value
<StoreContext.Provider value={{ state, dispatch }}>
...
</StoreContext.Provider>
问题出在你的 reducer 上,特别是你将之前的状态分配给 newState
以进行突变和 return 更新后的状态。在 JavaScript 中,非基本类型是通过地址而不是值来引用的。由于你的 initialState
这是一个数组恰好是一个非原始的,所以当你将一个非原始的分配给一个新的变量时,这个变量只指向内存中现有的数组并且不会创建新的副本。并且,在 React 中,仅当状态被重建时(这就是 React 理解存在更新的方式)而不是软变异时,更新才 triggered/broadcasted 。当你改变 return 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
}
}
我很清楚你在说什么,但 reducer 中的问题是只有可变方法才适用于状态。像 .slice()、.concat() 甚至扩展运算符 [...state] 这样的不可变方法不起作用,我不知道为什么 :( 我尝试了这两个答案,但 dispatch(action) 并没有改变状态。也许初始状态是问题所在,我会尽量把它写成
初始状态={ 购物车:[ 产品名称: '', 计数:0 ] }