React应用使用Hooks与ref实现卡片选中状态自动切换
React卡片选中状态交互优化方案
问题背景
当前通过索引控制卡片选中高亮的逻辑存在体验缺陷:
- 选中卡片A后,无法直接点击卡片B切换选中,必须先再次点击A取消选中才能选B
- 缺少点击卡片区域外部自动取消选中的常规交互
需要基于React Hooks与ref实现符合常规使用习惯的选中交互。
原有代码实现
父组件状态定义
const [isSelected, setIsSelected] = useState(""); function handleParamChange(e) { e.preventDefault(); const param = e.target.name //name may be desc const value = e.target.value setParams(prevParams => { return { ...prevParams, [param]: value} }) }
列表渲染逻辑
{ jobs.length > 0 && jobs.map( (job, index) => <JobCard key={job.id} job={job} index={index + 1} isSelected={isSelected} setIsSelected={setIsSelected} />) }
JobCard子组件原有实现
const JobCard = ({ setIsSelected, isSelected, index, job }) => { const [open, setOpen] = useState(false) const [isActive, setIsActive] = useState(false); console.log(isSelected) return ( <CardContainer> <CardPrimary onClick={() => { if (!isSelected) { setIsSelected(index); setIsActive(true); } else if (isSelected === index) { setIsSelected(""); setIsActive(false); } }} style={{ backgroundColor: isActive ? "#0062ff" : "inherit", display: "flex", height: "90%", width: "95%", borderRadius: "10px", justifyContent: "center", flexDirection: "column", boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.25)" }} > <CardHeader> <CardHeaderTopRow> <Typography variant = "cardheader1" color={isActive ? "white" : "inherit"}> {job.title} </Typography> <HeartDiv> <IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" onClick={()=> setOpen(prevOpen => !prevOpen)}> <FavoriteIcon fontSize="inherit" /> </IconButton> </HeartDiv> </CardHeaderTopRow> <Typography variant = "subtitle4" color={isActive ? "#d6d6d6" : "text.secondary"}> {job.company.display_name} </Typography> </CardHeader> </CardContainer> ) } export default JobCard
优化后实现
核心改动点
- 修复卡片点击判断逻辑,点击非当前选中卡片时直接更新索引完成切换,无需先清空原有选中状态
- 删除子组件冗余的
isActive本地状态,直接通过父组件传入的选中索引与当前卡片索引对比推导激活状态,避免多源状态不同步 - 用
useRef绑定卡片列表外层容器,配合全局点击监听实现点击外部区域自动清空选中 - 给卡片内的收藏按钮加事件冒泡拦截,避免点击操作触发卡片选中逻辑
父组件修改
- 引入
useRef、useEffect依赖,创建列表容器ref,添加外部点击判断逻辑 - 给卡片列表外层包裹容器绑定ref
import { useState, useRef, useEffect } from 'react'; // 父组件内 const [isSelected, setIsSelected] = useState(""); const cardListRef = useRef(null); // 监听全局点击,判断点击在卡片列表外部时清空选中 useEffect(() => { function handleClickOutside(e) { if (cardListRef.current && !cardListRef.current.contains(e.target)) { setIsSelected(""); } } document.addEventListener('mousedown', handleClickOutside); // 组件卸载时移除监听,防止内存泄漏 return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); // 列表渲染部分,外层包裹容器绑定ref <div ref={cardListRef}> { jobs.length > 0 && jobs.map( (job, index) => <JobCard key={job.id} job={job} index={index + 1} isSelected={isSelected} setIsSelected={setIsSelected} />) } </div>
JobCard子组件修改
const JobCard = ({ setIsSelected, isSelected, index, job }) => { const [open, setOpen] = useState(false) // 直接通过props推导激活状态,删除冗余本地状态 const isActive = isSelected === index; return ( <CardContainer> <CardPrimary onClick={() => { // 点击当前已选中卡片则取消选中,否则直接切换选中到当前卡片 isActive ? setIsSelected("") : setIsSelected(index); }} style={{ backgroundColor: isActive ? "#0062ff" : "inherit", display: "flex", height: "90%", width: "95%", borderRadius: "10px", justifyContent: "center", flexDirection: "column", boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.25)" }} > <CardHeader> <CardHeaderTopRow> <Typography variant = "cardheader1" color={isActive ? "white" : "inherit"}> {job.title} </Typography> <HeartDiv> <IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" // 阻止冒泡,避免点收藏按钮触发卡片选中 onClick={(e) => { e.stopPropagation(); setOpen(prevOpen => !prevOpen); }} > <FavoriteIcon fontSize="inherit" /> </IconButton> </HeartDiv> </CardHeaderTopRow> <Typography variant = "subtitle4" color={isActive ? "#d6d6d6" : "text.secondary"}> {job.company.display_name} </Typography> </CardHeader> </CardPrimary> </CardContainer> ) } export default JobCard
内容的提问来源于stack exchange,提问作者n_d22
相关产品推荐
相关产品推荐

