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
相关产品推荐
相关产品推荐

