javascriptreactjsstateshopping-cartreact-fullstack

how to add items to the cart without refresh in reactjs


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.


Solution

  • 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.

    Edit

    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.