React 卡片点击仅展开单个及30秒隐藏后重新触发显示问题求解
问题根因
- 你当前用了全局共享的
showMeta、visible布尔状态控制所有卡片的显隐,所以任意卡片触发状态变更会同步作用到所有卡片,导致全量展开 useTimeout在组件挂载时就直接启动30秒倒计时,30秒后visible被永久设为false,没有重置逻辑,也没有和展开行为绑定,所以后续点击无法重新触发显示
修复方案
我们将状态改为存储当前展开的城市ID,而非全局布尔值,同时将useTimeout的启动时机和展开行为绑定,倒计时结束后自动清空当前展开ID,实现复用:
- 替换原有全局显隐状态,用
expandedCityId存储当前展开的城市唯一标识 - 调整
useTimeout的延迟参数,仅当有展开的城市时才启动30秒倒计时,倒计时结束后清空展开ID - 卡片显隐逻辑改为判断当前卡片ID是否和
expandedCityId匹配,仅匹配的卡片展开 - 额外修复原代码中直接对
cities调用sort修改原数组的副作用问题,改为拷贝后排序
修复后的核心代码
import React from "react"; import { FaPlus } from "react-icons/fa"; import useTimeout from "../../../common/useTimeout"; import { Modal } from "../../../common/Modal"; import { ToggleState } from "../../../common/Toggle"; import { BsSearch } from "react-icons/bs"; import AdvancedFilter from "./AdvancedFilter"; export default function CitiesList({ cities }: { cities: any }): JSX.Element { const [filter, setFilter] = React.useState(""); const [sortType, setSortType] = React.useState(""); // 存储当前展开的城市ID,null表示无展开卡片 const [expandedCityId, setExpandedCityId] = React.useState<number | string | null>(null); console.log(filter); // 修复原sort修改原数组的问题,先拷贝再排序 const sorted = [...cities].sort((a: { name: string }, b: { name: any }) => { const isReversed = sortType === "asc" ? 1 : -1; return isReversed * a.name.localeCompare(b.name); }); const onSort = (sortType: React.SetStateAction<string>) => { console.log("changed"); setSortType(sortType); }; // 倒计时结束后清空展开ID const hide = () => setExpandedCityId(null); // 仅当有展开的城市时才启动30秒倒计时,否则不启动 useTimeout(hide, expandedCityId ? 30000 : null); console.log("CITIES", cities); console.log({ expandedCityId }); return ( <div style={{ marginTop: "3rem" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "20px", }} > <div>List of cities</div> <div style={{ display: "flex", alignItems: "center" }}> <div style={{ marginRight: "1rem" }}> <ToggleState render={({ isOpen, open, close }) => { return ( <> <button type="button" className="btn btn-primary" onClick={() => { isOpen ? close() : open(); }} > Advanced Filter </button> <Modal isActive={isOpen} modalContentWidth={"30%"} header={() => "Advanced Filter"} close={() => close()} renderBody={() => { return <AdvancedFilter close={() => close()} />; }} ></Modal> </> ); }} /> </div> <div style={{ position: "relative", marginRight: "1rem" }}> <input type="text" placeholder="Filter" name="namePrefix" style={{ padding: "0.35rem" }} onChange={(e: any) => { setFilter(e.target.value); }} /> <div style={{ position: "absolute", top: "5px", right: "5px" }}> <BsSearch size="16" /> </div> </div> <div style={{ width: "8rem" }}> <div className="btn-group"> <button type="button" className="btn dropdown-toggle sort-button" data-bs-toggle="dropdown" aria-expanded="false" > {sortType === "asc" ? "Ascending" : sortType === "desc" ? "Descending" : "Select"} </button> <ul className="dropdown-menu sort-button"> <li> <button className="dropdown-item" type="button" onClick={() => onSort("asc")} > Ascending </button> </li> <li> <button className="dropdown-item" type="button" onClick={() => onSort("desc")} > Descending </button> </li> </ul> </div> </div> </div> </div> <div> <div> <div className="row"> {cities && sorted.map((item: any, index: number) => ( <div className="col-lg-3" key={index}> <div className="card" style={{ textAlign: "center", display: "flex", justifyContent: "center", paddingBottom: "1rem", marginBottom: "1rem", marginRight: "1rem", }} > <div className="card-body"> <h5 className="card-title">{item.name}</h5> </div> {/* 仅当前卡片ID和展开ID匹配时显示详情 */} {expandedCityId === item.id ? ( <div> <p>Longitude: {item.longitude}</p> <p>Latitude: {item.latitude}</p> <p>Population: {item.population}</p> {/* <p>Time Zone: America</p> */} </div> ) : ( <div onClick={() => { // 点击时把当前卡片ID设为展开ID setExpandedCityId(item.id); }} style={{ cursor: "pointer" }} > <FaPlus size="18" /> </div> )} </div> </div> ))} </div> </div> </div> <div style={{ marginTop: "30px", display: "flex", justifyContent: "center" }} > {cities && cities.length > 10 ? ( <button className="secondary-button">Load More</button> ) : ( <p>There are no more cities</p> )} </div> </div> ); }
useTimeout代码无需改动。
效果说明
- 点击单个卡片的加号,仅对应ID匹配的卡片会展开详情,其余卡片保持收起状态
- 卡片展开后自动启动30秒倒计时,倒计时结束后自动收起
- 收起后再次点击任意卡片的加号,会重新触发对应卡片展开和30秒倒计时,无需刷新页面
内容的提问来源于stack exchange,提问作者rafi
相关产品推荐
相关产品推荐

