I am working on implementing shopping cart functionality in a React application, and while I have made some progress, I encountered challenges in managing the application's state effectively. The application consists of two main components: BookList and Navbar. The BookList component displays a list of books, each with an "Add to Cart" button, while the Navbar component contains a cart icon that shows the total number of items in the cart.
Currently, I store the cart items in localStorage. My goal is to ensure that items are added to the cart seamlessly, without the need to refresh the page, and that the cart count in the Navbar is updated dynamically as items are added.
BookList.jsx:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import {
Card, CardBody, Image, Text, Stack, Heading, Input, Center, InputGroup, InputLeftElement, Box, IconButton
} from '@chakra-ui/react';
import { SearchIcon } from '@chakra-ui/icons';
import { BsCartPlusFill } from "react-icons/bs";
export const BookList = () => {
const navigate = useNavigate();
const [data, setData] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [cartData, setCartData] = useState(() => {
const savedCart = localStorage.getItem('cartData');
return savedCart ? JSON.parse(savedCart) : [];
});
useEffect(() => {
const fetchBooks = async () => {
try {
const response = await axios.get('http://localhost:5000/api/books');
setData(response.data.data);
} catch (err) {
console.log(err);
}
};
fetchBooks();
}, []);
const filteredBooks = data.filter(book =>
book.title.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleAddToCart = (book) => {
const existingCartItem = cartData.find(cartItem => cartItem.id === book._id);
if (existingCartItem) {
const updatedCartData = cartData.map(cartItem =>
cartItem.id === book._id ? { ...cartItem, quantity: cartItem.quantity + 1 } : cartItem
);
setCartData(updatedCartData);
localStorage.setItem('cartData', JSON.stringify(updatedCartData));
window.location.reload();
} else {
const newCartItem = {
id: book._id,
title: book.title,
image: book.cover,
quantity: 1,
};
const updatedCartData = [...cartData, newCartItem];
setCartData(updatedCartData);
localStorage.setItem('cartData', JSON.stringify(updatedCartData));
window.location.reload();
}
};
return (
<div>
<Center>
<Text fontSize={'3xl'} fontWeight={'bold'} marginTop={'50px'}>
What Are you looking For ..?
</Text>
</Center>
<Center>
<InputGroup width="30%" margin={'30px'}>
<InputLeftElement pointerEvents='none'>
<SearchIcon color='black' />
</InputLeftElement>
<Input
placeholder="Search by book name"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
mb="10px"
size='md'
/>
</InputGroup>
</Center>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-around', marginTop: '20px' }}>
{filteredBooks.length > 0 ? (
filteredBooks.map((book) => (
<Box
position="relative"
key={book._id}
style={{ margin: '20px' }}
cursor="pointer"
role="group"
_hover={{
transform: 'scale(1.05)',
transition: 'all 0.4s ease-in-out',
boxShadow: 'xl',
}}
>
<Card
maxW="sm"
_groupHover={{
transform: 'scale(1.05)',
transition: 'all 0.4s ease-in-out',
boxShadow: 'xl',
}}
onClick={() => {
navigate(`/book/bookdetails/${book._id}`);
}}
>
<CardBody>
<Image
src={book.cover}
alt="Book cover"
borderRadius="xl"
boxSize="500px"
/>
<Stack mt="6" spacing="3">
<Heading size="md">
{book.title} <span style={{ color: 'green', fontSize: '15px' }}>${book.price}</span>
</Heading>
<Text>{book.author}</Text>
</Stack>
</CardBody>
</Card>
<IconButton
aria-label="Add to cart"
icon={<BsCartPlusFill />}
position="absolute"
bottom="5%"
right="5%"
colorScheme="green"
borderRadius="full"
size="lg"
_groupHover={{
transform: 'scale(1.4)',
transition: 'all 0.4s ease-in-out',
bottom: '3%',
right: '3%',
}}
onClick={(e) => {
e.stopPropagation();
handleAddToCart(book);
}}
/>
</Box>
))
) : (
<p>No books available :(</p>
)}
</div>
</div>
);
};
Navbar.jsx:
import React, { useState } from 'react';
import { Flex, Box, Heading, UnorderedList, ListItem, Link, Button, IconButton, Text, Image, Badge } from '@chakra-ui/react';
import { Link as RouterLink } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { FaCartShopping } from "react-icons/fa6";
import { MdDelete } from "react-icons/md";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
} from '@chakra-ui/react';
export const Navbar = () => {
const navigate = useNavigate();
const isUserSignIn = !!localStorage.getItem('token');
const getCartDataArr = JSON.parse(localStorage.getItem('cartData'));
const [cartItemCount, SetCartItemCount] = useState(getCartDataArr ? getCartDataArr.length : 0);
const handleSignOut = () => {
localStorage.removeItem('token');
navigate('/login');
};
const handleRemoveFromCart = (itemId) => {
const cartData = JSON.parse(localStorage.getItem('cartData'));
const itemIndex = cartData.findIndex(cartItem => cartItem.id === itemId);
if (itemIndex > -1) {
if (cartData[itemIndex].quantity > 1) {
cartData[itemIndex].quantity -= 1;
} else {
cartData.splice(itemIndex, 1);
}
localStorage.setItem('cartData', JSON.stringify(cartData));
SetCartItemCount(cartData.length);
window.location.reload();
}
};
return (
<Box bg='#ffffff' color='white'>
<Flex
justify='space-between'
align='center'
px={8}
py={4}
height='80px'
maxW='1200px'
mx='auto'
>
<Box>
<Heading as={RouterLink} to="/" fontSize='2xl' color={'#2D3748'} _hover={{ color: '#005bc8' }}>
Logo
</Heading>
</Box>
<UnorderedList display='flex' listStyleType='none' m={0} gap='20px' alignItems='center'>
{isUserSignIn ? (
<>
<ListItem display="flex" alignItems="center">
<Popover>
<PopoverTrigger>
<Box position="relative">
<Button variant={'ghost'} p={0}>
<FaCartShopping size="20px" />
</Button>
<Badge
position="absolute"
top="-8px"
right="-8px"
bg="red.500"
borderRadius="full"
px={2}
fontSize="0.8em"
color="white"
>
{cartItemCount}
</Badge>
</Box>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
{getCartDataArr && getCartDataArr.length > 0 ? (
getCartDataArr.map((item) => (
<Box key={item.id} marginTop={'10%'}>
<Flex alignItems="center" justifyContent="space-between" mb={2} backgroundColor={'#f6f6f6'} padding={'10px'}>
<Image
src={item.image} // Ensure this matches your object property
alt="Cart Item Image"
boxSize="50px"
objectFit="contain"
maxH="50px"
maxW="50px"
borderRadius="md"
/>
<Text color={'black'} ml={2}>{item.title} x{item.quantity}</Text>
<IconButton aria-label="Delete item" icon={<MdDelete color='red' />} onClick={() => handleRemoveFromCart(item.id)} variant="ghost" />
</Flex>
</Box>
))
) : (
<Text>No items in the cart.</Text>
)}
<Button backgroundColor={'#85ff8d'} _hover={{ backgroundColor: "#41ff4e" }} width="100%" mt={4}>
Proceeding
</Button>
</PopoverBody>
</PopoverContent>
</Popover>
</ListItem>
<ListItem>
<Link as={RouterLink} to="/account" color={'#2D3748'} _hover={{ backgroundColor: "#000", color: '#fff' }}>
Account
</Link>
</ListItem>
<ListItem>
<Link as={RouterLink} to="/login" color={'#2D3748'} _hover={{ backgroundColor: "#000", color: '#fff' }} onClick={handleSignOut}>
Signout
</Link>
</ListItem>
</>
) : (
<>
<ListItem display="flex" alignItems="center">
<Popover>
<PopoverTrigger>
<Box position="relative">
<Button variant={'ghost'} p={0}>
<FaCartShopping size="20px" />
</Button>
<Badge
position="absolute"
top="-8px"
right="-8px"
bg="red.500"
borderRadius="full"
px={2}
fontSize="0.8em"
color="white"
>
{cartItemCount}
</Badge>
</Box>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
{getCartDataArr && getCartDataArr.length > 0 ? (
getCartDataArr.map((item) => (
<Box key={item.id} marginTop={'10%'}>
<Flex alignItems="center" justifyContent="space-between" mb={2} backgroundColor={'#f6f6f6'} padding={'10px'}>
<Image
src={item.image} // Ensure this matches your object property
alt="Cart Item Image"
boxSize="50px"
objectFit="contain"
maxH="50px"
maxW="50px"
borderRadius="md"
/>
<Text color={'black'} ml={1}>{item.title} x{item.quantity} </Text>
<IconButton aria-label="Delete item" icon={<MdDelete color='red' />} onClick={() => handleRemoveFromCart(item.id)} variant="ghost" />
</Flex>
</Box>
))
) : (
<Text>No items in the cart.</Text>
)}
<Button backgroundColor={'#85ff8d'} _hover={{ backgroundColor: "#41ff4e" }} width="100%" mt={4}>
Proceeding
</Button>
</PopoverBody>
</PopoverContent>
</Popover>
</ListItem>
<ListItem>
<Link as={RouterLink} to="/login" color={'#2D3748'} _hover={{ backgroundColor: "#000", color: '#fff' }}>
Login
</Link>
</ListItem>
<ListItem>
<Link as={RouterLink} to="/signup" color={'#2D3748'} _hover={{ backgroundColor: "#000", color: '#fff' }}>
Signup
</Link>
</ListItem>
</>
)}
</UnorderedList>
</Flex>
</Box>
);
};
if you need from me to provide more informations just let me know.
With react, if you ever need to call window.reload
you're doing it wrong.
Anytime a value returned from a useState
hook is updated the page will rerender and anywhere that value is consumed will be updated.
Take this simple component for example.
function Counter(){
const [count, setCount] = useState(0)
return <button onClick={()=> setCount(prev=> prev+1)}>Click Count: {count}</button>
}
Anytime you click the button to update state, react will re-render the component and the count
of the button will update on the page. The same is true for all state. So if you have something like this
const [cartData, setCartData] = useState([])
anytime you call setCartData((pre => ([...pre, ....your new data....]))
you can consume that state in your JSX like so and the page will update as you add or remove values by calling setCartData
<div>Cart Count {cartData.length}</div>
Persisting state in local storage is a very common pattern, many people write a custom hook that synchronizes app state on initial render by reading from localStorage. Then everytime state updates it also writes to localStorage.
You've provided a lot of code, some of which already applies the state subscription strategy I outlined above. I can only assume that you have copied this code from somewhere else and are attempting to add functionality. I would start with a small app and learn the basics of useState and useEffect and experiment with how they work. Causing the page to re-render by subscribing to state is the most basic concept of react.
here is a link to a playground demonstrating a basic app that has state lifted up to a common parent so two sibling components can read a common state value.
https://stackblitz.com/edit/vitejs-vite-uube3y?file=src%2Fassets%2Fbook-list.tsx
Here is an example of a parent component that shares state with two sibling components. App
constains the state, and both NavBar
and BookList
consume the state. And BookList
can also update the state.
import { useState } from 'react';
import './App.css';
import NavBar from './assets/nav-bar';
import BookList, { Cart } from './assets/book-list';
function App() {
const [cart, setCart] = useState<Cart[]>([]);
return (
<div>
<NavBar cart={cart} />
<BookList cart={cart} setCart={setCart} />
</div>
);
}
export default App;
Knowing when to lift state up, verses when to reach for a global state manager is a difficult one. I usually try to lift state up first, but often that can lead to a lot of prop drilling to get that state back down to the components that need to consume it. My rule of thump is if I'm prop drilling more than 2 layers, I'll start thinking about state management. I use both the react context api and zustand.