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

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绑定卡片列表外层容器,配合全局点击监听实现点击外部区域自动清空选中
  • 给卡片内的收藏按钮加事件冒泡拦截,避免点击操作触发卡片选中逻辑

父组件修改

  1. 引入useRef、useEffect依赖,创建列表容器ref,添加外部点击判断逻辑
  2. 给卡片列表外层包裹容器绑定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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 20:09:11