You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React实现无刷新添加商品至购物车及动态更新数量方案

问题解决:React购物车无刷新动态更新

核心问题分析

当前代码中window.location.reload()导致页面强制刷新,同时BookList和Navbar各自维护独立的购物车状态,无法实时同步,这是需要解决的核心问题。

解决方案步骤

1. 移除强制刷新代码

在BookList.jsx的handleAddToCart和Navbar.jsx的handleRemoveFromCart中删除window.location.reload()语句,这是消除页面刷新的第一步。

2. 使用React Context共享购物车状态

通过创建全局购物车Context,让BookList和Navbar共享同一状态,彻底解决组件间状态同步问题,同时自动将状态同步到localStorage保证数据持久化。

步骤1:创建CartContext.js

import React, { createContext, useContext, useState, useEffect } from 'react';

const CartContext = createContext();

export const CartProvider = ({ children }) => {
  // 从localStorage初始化购物车状态
  const [cartData, setCartData] = useState(() => {
    const savedCart = localStorage.getItem('cartData');
    return savedCart ? JSON.parse(savedCart) : [];
  });

  // 状态变化时自动同步到localStorage
  useEffect(() => {
    localStorage.setItem('cartData', JSON.stringify(cartData));
  }, [cartData]);

  // 添加商品到购物车逻辑
  const addToCart = (book) => {
    setCartData(prev => {
      const existingItem = prev.find(item => item.id === book._id);
      if (existingItem) {
        return prev.map(item => 
          item.id === book._id ? {...item, quantity: item.quantity + 1} : item
        );
      } else {
        return [...prev, {
          id: book._id,
          title: book.title,
          image: book.cover,
          quantity: 1
        }];
      }
    });
  };

  // 移除商品逻辑(减少数量或删除)
  const removeFromCart = (itemId) => {
    setCartData(prev => {
      const updatedCart = prev.map(item => {
        if (item.id === itemId) {
          return item.quantity > 1 ? {...item, quantity: item.quantity - 1} : null;
        }
        return item;
      }).filter(item => item !== null);
      return updatedCart;
    });
  };

  // 计算购物车商品总数量
  const getCartItemCount = () => {
    return cartData.reduce((total, item) => total + item.quantity, 0);
  };

  return (
    <CartContext.Provider value={{ cartData, addToCart, removeFromCart, getCartItemCount }}>
      {children}
    </CartContext.Provider>
  );
};

export const useCart = () => useContext(CartContext);

步骤2:在App.js中包裹CartProvider

import { CartProvider } from './CartContext';
import { Navbar } from './Navbar';
import { BookList } from './BookList';

function App() {
  return (
    <CartProvider>
      <Navbar />
      <BookList />
      {/* 其他路由组件 */}
    </CartProvider>
  );
}

export default App;

步骤3:修改BookList.jsx使用Context

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";
import { useCart } from './CartContext'; // 导入Context钩子

export const BookList = () => {
  const navigate = useNavigate();
  const [data, setData] = useState([]); 
  const [searchTerm, setSearchTerm] = useState('');   
  const { addToCart } = useCart(); // 获取添加购物车方法

  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) => {
    addToCart(book);
  };

  // 保留原有的JSX返回内容
  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>
  );
};

步骤4:修改Navbar.jsx使用Context

import React 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';
import { useCart } from './CartContext'; // 导入Context钩子

export const Navbar = () => {
  const navigate = useNavigate();
  const isUserSignIn = !!localStorage.getItem('token');
  const { cartData, removeFromCart, getCartItemCount } = useCart(); // 获取购物车数据和方法

  const handleSignOut = () => {
    localStorage.removeItem('token');
    navigate('/login');
  };

  // 简化移除商品逻辑
  const handleRemoveFromCart = (itemId) => {
    removeFromCart(itemId);
  };

  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"
                      >
                        {getCartItemCount()} {/* 使用Context提供的计数方法 */}
                      </Badge>
                    </Box>
                  </PopoverTrigger>
                  <PopoverContent>
                    <PopoverBody>
                      {cartData.length > 0 ? (
                        cartData.map((item) => (
                          <Box key={item.id} marginTop={'10%'}>
                            <Flex alignItems="center" justifyContent="space-between" mb={2} backgroundColor={'#f6f6f6'} padding={'10px'}>
                              <Image 
                                src={item.image}
                                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"
                      >
                        {getCartItemCount()} {/* 使用Context提供的计数方法 */}
                      </Badge>
                    </Box>
                  </PopoverTrigger>
                  <PopoverContent>
                    <PopoverBody>
                      {cartData.length > 0 ? (
                        cartData.map((item) => (
                          <Box key={item.id} marginTop={'10%'}>
                            <Flex alignItems="center" justifyContent="space-between" mb={2} backgroundColor={'#f6f6f6'} padding={'10px'}>
                              <Image 
                                src={item.image}
                                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>
  );
};

关键改进点

  • 彻底移除页面刷新逻辑,实现无刷新操作
  • 通过Context实现购物车状态全局共享,组件间状态实时同步
  • 自动同步状态到localStorage,保证页面刷新后数据不丢失
  • 购物车数量统计改为计算所有商品的总数量,更符合用户使用预期

内容的提问来源于stack exchange,提问作者Pv33_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 16:27:32