React闪卡应用点击功能异常:单次禁用/五次触发图片搜索失效
问题描述
我给儿子做了一款React闪卡应用,需求是:点击卡片时禁用该卡片;如果同一卡片(比如“Mercury”)被点击五次,就打开对应物品的Google图片搜索页面。但这个功能现在没法正常工作。
原代码
import React, { useState, useEffect } from "react"; import { Link } from "react-router-dom"; import { getColor } from "./utility/colorUtils.js"; import { playAudio } from "./utility/audioUtils.js"; import { getPageName, goHome } from "./utility/pageUtils.js"; import "./Category.css"; const Category = ({ type }) => { const [data, setData] = useState([]); const [clickedItemName, setClickedItemName] = useState(""); const [clickedItems, setClickedItems] = useState([]); const [slideText, setSlideText] = useState(""); const [clickCounts, setClickCounts] = useState({}); useEffect(() => { const jsonData = require(`./data/${type}.json`); setData(jsonData); }, [type]); useEffect(() => { playAudio(clickedItems.length, data.length, goHome); }, [clickedItems.length, data.length]); const handleClick = item => { setSlideText(""); const thumbnailId = item.name.replace(/\s+/g, "-").toLowerCase(); const clickedThumbnail = document.getElementById(`${thumbnailId}`); setClickedItemName(item.name); if (!clickedThumbnail.classList.contains("disabled")) { speechSynthesis.speak(new SpeechSynthesisUtterance(item.name)); clickedThumbnail.classList.add("disabled"); setClickedItems(prev => [...prev, thumbnailId]); console.log(clickedItems); // Update clickCounts for this item setClickCounts(prevClickCounts => ({ ...prevClickCounts, [item.id]: (prevClickCounts[item.id] || 0) + 1, })); console.log(clickCounts); switch (clickedItems.length + 1) { case 1: setSlideText("🎈"); break; case 5: setSlideText("🪄"); break; case 10: setSlideText("🎁"); break; case 15: setSlideText("✨"); break; case 20: setSlideText("⭐️"); break; case 25: setSlideText("🎖️"); break; default: setSlideText(""); break; } } if ((clickCounts[item.id] || 0) === 5) { window.open(`https://www.google.com/images?q=${encodeURIComponent(item.name)}`, "_blank"); setClickCounts(prevClickCounts => ({ ...prevClickCounts, [item.id]: 0, })); } }; return ( <div className="category-page" style={{ overflowX: "hidden" }}> <nav className="breadcrumbs"> <Link to="/" className="home-link"> Go Home </Link> <span onClick={() => goHome()} className="breadcrumb-divider"> {" "} / </span> <span onClick={() => goHome()} className="current-page"> <img src={`/images/types/${type}.png`} alt="Icon" className="icon" width="20" /> {getPageName(type)} </span> <span className="float-right selected"> <a href={`https://www.google.com/images?q=${encodeURIComponent(clickedItemName)}`} target="_blank" rel="noopener noreferrer"> "{clickedItemName}" </a> </span> </nav> <div className={`thumbnails-container`}> {data.map((item, index) => ( <div key={index} id={`${item.name.replace(/\s+/g, "-").toLowerCase()}`} className="thumbnail" onClick={() => handleClick(item)} style={{ backgroundColor: getColor(item), }} > <div className="name-initial">{item.name.charAt(0)}</div> <div className={`thumbnail-details`}> <h6>{item.name}</h6> </div> </div> ))} </div> {slideText && ( <div className="slide-up"> <p>{slideText}</p> </div> )} {data.length === clickedItems.length && ( <div className="achievement-container"> <img src="/images/1.gif" alt="achievement" className="achievement" /> </div> )} </div> ); }; export default Category;
问题根源
- 状态更新异步性:React的
setState是异步操作,更新clickCounts后立刻读取该状态,拿到的还是旧值,导致五次点击的判断永远无法触发。 - 禁用逻辑冲突:原代码第一次点击就给卡片添加
disabled类,之后无法再点击,自然达不到五次点击的条件。
修复方案及代码
修改后的代码
import React, { useState, useEffect } from "react"; import { Link } from "react-router-dom"; import { getColor } from "./utility/colorUtils.js"; import { playAudio } from "./utility/audioUtils.js"; import { getPageName, goHome } from "./utility/pageUtils.js"; import "./Category.css"; const Category = ({ type }) => { const [data, setData] = useState([]); const [clickedItemName, setClickedItemName] = useState(""); const [clickedItems, setClickedItems] = useState([]); const [slideText, setSlideText] = useState(""); const [clickCounts, setClickCounts] = useState({}); useEffect(() => { const jsonData = require(`./data/${type}.json`); setData(jsonData); }, [type]); useEffect(() => { playAudio(clickedItems.length, data.length, goHome); }, [clickedItems.length, data.length]); const handleClick = item => { setSlideText(""); const thumbnailId = item.name.replace(/\s+/g, "-").toLowerCase(); const clickedThumbnail = document.getElementById(`${thumbnailId}`); setClickedItemName(item.name); // 先获取当前点击次数,计算新次数 const currentCount = clickCounts[item.id] || 0; const newCount = currentCount + 1; speechSynthesis.speak(new SpeechSynthesisUtterance(item.name)); // 更新点击计数 setClickCounts(prev => ({ ...prev, [item.id]: newCount })); // 判断是否达到五次点击 if (newCount === 5) { window.open(`https://www.google.com/images?q=${encodeURIComponent(item.name)}`, "_blank"); // 重置计数并禁用卡片 setClickCounts(prev => ({ ...prev, [item.id]: 0 })); clickedThumbnail.classList.add("disabled"); // 将卡片加入已完成列表 setClickedItems(prev => [...prev, thumbnailId]); } // 计算总点击次数,更新滑动文本 const totalClicks = Object.values(clickCounts).reduce((sum, count) => sum + count, 0) + 1; switch (totalClicks) { case 1: setSlideText("🎈"); break; case 5: setSlideText("🪄"); break; case 10: setSlideText("🎁"); break; case 15: setSlideText("✨"); break; case 20: setSlideText("⭐️"); break; case 25: setSlideText("🎖️"); break; default: setSlideText(""); break; } }; return ( <div className="category-page" style={{ overflowX: "hidden" }}> <nav className="breadcrumbs"> <Link to="/" className="home-link"> Go Home </Link> <span onClick={() => goHome()} className="breadcrumb-divider"> {" "} / </span> <span onClick={() => goHome()} className="current-page"> <img src={`/images/types/${type}.png`} alt="Icon" className="icon" width="20" /> {getPageName(type)} </span> <span className="float-right selected"> <a href={`https://www.google.com/images?q=${encodeURIComponent(clickedItemName)}`} target="_blank" rel="noopener noreferrer"> "{clickedItemName}" </a> </span> </nav> <div className={`thumbnails-container`}> {data.map((item, index) => ( <div key={index} id={`${item.name.replace(/\s+/g, "-").toLowerCase()}`} className={`thumbnail ${clickCounts[item.id] === 5 ? "disabled" : ""}`} onClick={() => handleClick(item)} style={{ backgroundColor: getColor(item), }} > <div className="name-initial">{item.name.charAt(0)}</div> <div className={`thumbnail-details`}> <h6>{item.name}</h6> </div> </div> ))} </div> {slideText && ( <div className="slide-up"> <p>{slideText}</p> </div> )} {data.length === clickedItems.length && ( <div className="achievement-container"> <img src="/images/1.gif" alt="achievement" className="achievement" /> </div> )} </div> ); }; export default Category;
关键修改点
- 移除首次点击禁用卡片的逻辑,改为点击满五次后再禁用卡片,确保能完成五次点击触发条件。
- 提前计算当前和新的点击次数,基于新次数判断是否触发图片搜索,避开异步状态更新的问题。
- 在JSX中通过状态动态添加
disabled类,保持React状态与DOM的一致性,减少直接操作DOM的副作用。 - 调整总点击次数的计算方式,保证滑动文本的触发逻辑正确。
内容的提问来源于stack exchange,提问作者code-8
相关产品推荐
相关产品推荐

