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

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">
                    {" "}
                    &nbsp; &nbsp; &nbsp; / &nbsp; &nbsp; &nbsp;
                </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;

问题根源

  1. 状态更新异步性:React的setState是异步操作,更新clickCounts后立刻读取该状态,拿到的还是旧值,导致五次点击的判断永远无法触发。
  2. 禁用逻辑冲突:原代码第一次点击就给卡片添加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">
                    {" "}
                    &nbsp; &nbsp; &nbsp; / &nbsp; &nbsp; &nbsp;
                </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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 21:15:04