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

React+MUI购物车表格:如何动态使用useRef实现行删除动画?

解决TableRow删除动画仅第一行生效的问题

问题根源在于你用了单个useRef绑定所有TableRow,每次循环渲染时ref会被后续元素覆盖,导致只能获取到某一行的DOM元素。要给每个行绑定独立的ref,需要用ref对象存储每个行的DOM元素,具体修改如下:

1. 重构Ref存储结构

将单个ref改为按商品id存储的对象,确保每个行的ref独立:

const tableRowRefs = useRef<Record<number, HTMLTableRowElement | null>>({});

2. 为每个TableRow绑定对应Ref

使用函数式ref将每个行的DOM元素存入ref对象,对应item的唯一id:

<TableRow 
  key={item.id} 
  ref={(element) => { tableRowRefs.current[item.id] = element; }}
>

3. 修改删除函数,获取对应行元素

在handleDeleteItem中根据id取出对应的行,执行动画后再触发Redux删除操作:

const handleDeleteItem = (id: number) => {
  const tableRow = tableRowRefs.current[id];
  if (tableRow) {
    tableRow.style.animation = 'removeTableRow 1s';
    setTimeout(() => {
      tableRow.style.animation = '';
      dispatch(removeFromCart(id));
      // 清理已删除行的ref,避免内存泄漏
      delete tableRowRefs.current[id];
    }, 1000);
  }
}

4. 确保动画CSS全局生效

别忘了在全局样式文件(比如index.css)中定义removeTableRow动画,否则动画不会生效:

@keyframes removeTableRow {
  0% {
    opacity: 1;
    height: 56px; /* 匹配MUI TableRow默认高度 */
  }
  100% {
    opacity: 0;
    height: 0;
    padding: 0;
  }
}

修改后的完整代码

import { Typography, TableContainer, Table, TableBody, TableHead, TableRow, TableCell, Paper, Stack, Grid, Button, IconButton } from "@mui/material";
import ShoppingCartCheckoutIcon from '@mui/icons-material/ShoppingCartCheckout';
import RemoveShoppingCartIcon from '@mui/icons-material/RemoveShoppingCart';
import { createTheme, ThemeProvider } from "@mui/material/styles";
import { NavBar } from "../components/navbar";
import { useSelector, useDispatch } from "react-redux";
import type { RootState } from "../redux/store";
import { clearCart, removeFromCart } from "../redux/slices/cartSlice";
import { useRef } from "react";

const customTheme = createTheme({
    typography: {
        fontFamily: [
            'Satisfy',
            'cursive'
        ].join(','),
    }
})

export const CartPage = () => {
    // 改为按id存储的ref对象
    const tableRowRefs = useRef<Record<number, HTMLTableRowElement | null>>({});

    const dispatch = useDispatch();
    const cartItems = useSelector((state: RootState) => state.cart.cart);

    const handleDeleteItem = (id: number) => {
        const tableRow = tableRowRefs.current[id];
        if (tableRow) {
            tableRow.style.animation = 'removeTableRow 1s';
            setTimeout(() => {
                tableRow.style.animation = '';
                dispatch(removeFromCart(id));
                // 清理已删除行的ref
                delete tableRowRefs.current[id];
            }, 1000);
        }
    }

    return (
        <div>
            <NavBar/>
            <Stack direction='row' sx={{flexWrap: 'wrap'}} className='cartPage'>
                <TableContainer className="scrollableTable" component={Paper} sx={{ width:'50vw', height: '92vh'}}>
                    <Table stickyHeader>
                        <TableHead>
                            <TableRow>
                                <TableCell sx={{ fontSize: '25px' }}><b>Where?</b></TableCell>
                                <TableCell sx={{ fontSize: '25px' }}><b>When?</b></TableCell>
                                <TableCell sx={{ fontSize: '25px' }}><b>Room</b></TableCell>
                                <TableCell sx={{ fontSize: '25px' }}><b>Price</b></TableCell>
                                <TableCell></TableCell>
                            </TableRow>
                        </TableHead>
                        <TableBody>
                            {cartItems.map((item) => (
                                <TableRow 
                                    key={item.id} 
                                    ref={(element) => { tableRowRefs.current[item.id] = element; }}
                                >
                                    <TableCell sx={{ fontSize: '20px' }}>{item.name}</TableCell>
                                    <TableCell sx={{ fontSize: '20px' }}>{item.dates}</TableCell>
                                    <TableCell sx={{ fontSize: '20px' }}><i>{item.room}</i></TableCell>
                                    <TableCell sx={{ fontSize: '20px' }}>${item.price}</TableCell>
                                    <TableCell>
                                        <IconButton 
                                            color='error'
                                            onClick={() => {handleDeleteItem(item.id)}}
                                            >
                                                <ShoppingCartCheckoutIcon fontSize="large" />
                                            </IconButton>
                                        </TableCell>
                             </TableRow>
                         ))}
                     </TableBody>
                    </Table>
                </TableContainer>
                <Grid container justifyContent='center' sx={{width: '40vw'}}>
                    <Stack direction='column' justifyContent='space-evenly'>
                        <ThemeProvider theme={customTheme}> 
                            <Typography variant="h1" className="cartPageCheckoutTypography" sx={{ color: '#74BF8B', textAlign: 'center'}}>
                                Satisfied?
                            </Typography>
                        </ThemeProvider>
                        <Stack direction='row' spacing={1.5}>
                            <Button 
                            variant="contained" 
                            color='success' 
                            size="large" 
                            sx={{ backgroundColor: '#5DBF9A', color: 'white', fontSize: '20px', width: '13vw' }}>
                                Confirm purchase
                            </Button>
                            <Button 
                            variant="contained" 
                            color='success' 
                            size="large" 
                            sx={{ backgroundColor: '#91BE77', color: 'white', fontSize: '20px', width: '13vw' }}
                            href='/book'>
                                Continue booking
                            </Button>
                            <Button 
                            variant="contained" 
                            color='success' 
                            size="large" 
                            sx={{ backgroundColor: '#B9BD5C', color: 'white', fontSize: '20px', width: '13vw' }}
                            onClick={()=>{dispatch(clearCart())}}
                            endIcon={<RemoveShoppingCartIcon />}
                            >
                                Clear cart
                            </Button>
                        </Stack>
                    </Stack>
                </Grid>
            </Stack>
        </div>
    );
};

关键说明

  • 用Record<number, HTMLTableRowElement | null>类型约束ref对象,确保键为商品id,值为对应行的DOM元素。
  • 函数式ref可以动态将每个行的元素存入ref对象,避免单个ref被覆盖的问题。
  • 删除后清理ref对象中的对应项,避免不必要的内存占用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 06:54:58