I have a problem which I can't figure it out. I'm building an ecommerce react app and using useReducer
and useContext
for state management. Client opens a product, picks number of items and then click button "Add to Cart" which dispatches an action. This part is working well, and the problem starts. I don't know how to show and update in Navbar.js
component a total number of products in cart. It is showing after route changes, but I want it to update when clicking Add to Cart button. I tried useEffect but it doesn't work.
initial state looks like this
const initialState = [
{
productName: '',
count: 0
}
]
AddToCart.js works good
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 is where I have a problem
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
It seems you are mutating the state
object in your reducer function. You first save a reference to the state with const newState = state
, then mutate that reference with each newState[state.length] = .....
, and then return the same state reference for the next state with return newState
. The next state object is never a new object reference.
Consider the following that uses various array methods to operate over the state
array and return new array references:
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
should now see the state updates from the context.
const itemsSum = itemCounter.state
.map((item) => item.count)
.reduce((prev, curr) => prev + curr, 0);
It also appears you've memoized the state
value in a useMemo
hook with an empty dependency array. This means the counter
value passed to StoreContext.Provider
never updates.
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>
)
}
Either add state
to the dependency array
const counter = useMemo(() => ({ state, dispatch }), [state]);
Or don't memoize it at all and pass state
and dispatch
to the context value
<StoreContext.Provider value={{ state, dispatch }}>
...
</StoreContext.Provider>