React中如何实现鼠标悬停时仅作用于指定列表项?
问题原因
你当前将悬停状态isHover定义在父组件LabelList中,是所有列表项共享的单个布尔值:任意列表项触发鼠标进入事件将该值设为true时,所有绑定了这个状态的列表项都会同步响应渲染逻辑,自然会出现所有项一起变化的问题。
可选实现方案
方案1:纯CSS实现(无状态,最推荐)
如果只是实现悬停显示操作按钮的常规交互,完全不需要编写JS状态逻辑,借助CSS:hover伪类就能实现,性能最好代码量最少:
- 先加一段组件作用域的CSS:
/* 默认隐藏操作按钮容器 */ .list-item .hover-action-wrap { display: none; } /* 鼠标悬停在列表项上时,显示内部的操作按钮容器 */ .list-item:hover .hover-action-wrap { display: flex; }
- 删掉所有和悬停相关的JS状态、事件回调,直接渲染内容即可:
// LabelList组件简化后 const LabelList = ({labelIcon, createFolderIcon}) => { return ( <ul className='pl-3 pb-3 label-list'> <ListItem className='active' icon={labelIcon}> <p>February</p> <div className="ml-auto mr-3 hover-action-wrap"> <EditDelete /> </div> </ListItem> <ListItem icon={labelIcon}> <p>June</p> <div className="ml-auto mr-3 hover-action-wrap"> <EditDelete /> </div> </ListItem> <ListItem icon={createFolderIcon}> <p style={{color:"#6F76A7"}}>Create new label</p> </ListItem> </ul> ) } // ListItem组件简化后(不需要接收悬停相关props) const ListItem = ({ className, icon, children }) => { return ( <li className={`list-item flex align-middle items-center mb-1 ${className}`}> <ListItemIcon icon={icon}/> {children} </li> ) }
方案2:状态下沉到子组件自管理
如果悬停后需要触发JS逻辑(比如埋点、预加载数据等),可以把悬停状态放到每个ListItem组件内部维护,每个列表项的状态互相独立,不会互相干扰:
- 改写
ListItem组件,内部自己维护悬停状态:
import { useState } from 'react'; const ListItem = ({ className, icon, children }) => { // 每个ListItem实例有独立的isHover状态,互不影响 const [isHover, setIsHover] = useState(false); return ( <li className={`list-item flex align-middle items-center mb-1 ${className}`} onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} > <ListItemIcon icon={icon}/> {/* 把isHover状态传给子节点使用 */} {typeof children.type === 'function' ? React.cloneElement(children, { isHover }) : children} </li> ) }
- 父组件不需要维护任何悬停相关的状态和回调,子节点直接拿
isHover判断渲染即可。
方案3:父组件通过唯一标识区分悬停项
如果业务要求必须把悬停状态存在父组件统一管理,不要存布尔值,改成存储当前悬停项的唯一标识(可以是项的ID、自定义key、索引值),只有当前项的标识和存储的悬停标识一致时才渲染目标元素:
import { useState } from 'react'; const LabelList = ({labelIcon, createFolderIcon}) => { // 默认值为null,代表没有悬停的项,值为当前悬停项的唯一key const [hoveredKey, setHoveredKey] = useState(null); return ( <ul className='pl-3 pb-3 label-list'> <ListItem className='active' icon={labelIcon} onMouseEnter={() => setHoveredKey('feb')} onMouseLeave={() => setHoveredKey(null)} > <p>February</p> <div className="flex ml-auto mr-3"> {hoveredKey === 'feb' && <EditDelete />} </div> </ListItem> <ListItem icon={labelIcon} onMouseEnter={() => setHoveredKey('june')} onMouseLeave={() => setHoveredKey(null)} > <p>June</p> <div className="flex ml-auto mr-3"> {hoveredKey === 'june' && <EditDelete />} </div> </ListItem> <ListItem icon={createFolderIcon}> <p style={{color:"#6F76A7"}}>Create new label</p> </ListItem> </ul> ) } // ListItem组件只需要透传事件即可 const ListItem = ({ className, icon, children, onMouseEnter, onMouseLeave }) => { return ( <li className={`list-item flex align-middle items-center mb-1 ${className}`} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > <ListItemIcon icon={icon}/> {children} </li> ) }
注意:优先用
onMouseLeave代替onMouseOut,避免事件冒泡导致的悬停状态闪烁问题。
内容的提问来源于stack exchange,提问作者Damla Kara
相关产品推荐
相关产品推荐

