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

React状态更新需两次点击问题排查与修复求助

问题:点击Surah后需两次点击才生效的原因及修复方案

问题描述

React新手开发的古兰经播放器应用,可正常从API获取数据并设置状态,但点击Surah的div触发事件更新ayahsAudios和ayahsText状态时,必须点击两次才能生效。

核心问题分析

  1. 状态更新异步性:React的setState是异步操作,在handleSurah中调用setAyahsAudios(data.verses)后立刻执行changeAyah,此时ayahsAudios还未完成更新,第一次点击时使用的仍是旧状态(空数组),第二次点击时状态才更新完毕。
  2. DOM操作不规范:直接用document.querySelector获取audio和ayah元素,组件渲染周期内可能无法正确获取到最新的DOM节点,导致操作失效。
  3. 普通变量代替状态:ayahIndex是普通变量,组件重新渲染后会被重置为0,且无法触发视图更新;同时每次点击都绑定新的ended事件,导致事件重复触发,逻辑混乱。

修复方案

  1. 使用useRef获取DOM元素,确保能稳定访问组件内的audio和ayah节点。
  2. 将ayahIndex改为useState状态,让React追踪其变化并触发视图更新。
  3. 利用useEffect监听ayahsAudios的变化,当状态更新完成后自动执行changeAyah逻辑。
  4. 用useEffect管理audio的ended事件监听,在依赖变化时清理旧监听,避免重复绑定。
  5. 简化axios请求写法,统一使用async/await,避免then/catch混用。

修复后的完整代码

组件代码

import { useState, useEffect, useRef } from "react"
import axios from "axios"
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content'
import './QuranPlayer.scss'

const apiBaseUrl = "https://api.quran.gading.dev/surah/"

const QuranPlayer = () => {
    const [surahs, setSurahs] = useState([])
    const [ayahsText, setAyahsText] = useState([])
    const [ayahsAudios, setAyahsAudios] = useState([])
    const [ayahIndex, setAyahIndex] = useState(0)

    // 使用useRef获取DOM元素
    const audioRef = useRef(null)
    const ayahRef = useRef(null)

    const AlertSwal = withReactContent(Swal)

    useEffect(() => {
        const fetchSurahs = async () => {
            try {
                const response = await axios.get(apiBaseUrl)
                setSurahs(response.data.data)
            } catch (error) {
                console.log(error.toJSON())
            }
        }
        fetchSurahs()
    }, [])

    // 监听ayahsAudios变化,更新音频和经文
    useEffect(() => {
        if (ayahsAudios.length > 0) {
            changeAyah(ayahIndex)
        }
    }, [ayahsAudios, ayahIndex])

    // 管理audio的ended事件监听
    useEffect(() => {
        const audio = audioRef.current
        if (!audio) return

        const handleAudioEnded = () => {
            setAyahIndex(prev => {
                const newIndex = prev + 1
                if (newIndex === ayahsAudios.length) {
                    AlertSwal.fire(
                        '   إنتهت السورة 🕋',
                        '',
                        'success'
                    )
                }
                return newIndex < ayahsAudios.length ? newIndex : prev
            })
        }

        audio.addEventListener('ended', handleAudioEnded)
        // 清理函数,移除监听
        return () => audio.removeEventListener('ended', handleAudioEnded)
    }, [ayahsAudios, AlertSwal])

    const handleSurah = async (index) => {
        try {
            const response = await axios.get(`${apiBaseUrl}${index + 1}`)
            const data = response.data.data
            setAyahsAudios(data.verses)
            setAyahsText(data.verses)
            setAyahIndex(0) // 重置索引到第一个经文
        } catch (error) {
            console.log(error.toJSON())
        }
    }

    function changeAyah(index) {
        const audio = audioRef.current
        const ayahElement = ayahRef.current
        if (!audio || !ayahElement || !ayahsAudios[index]) return

        audio.src = ayahsAudios[index].audio.primary
        ayahElement.innerHTML = ayahsText[index].text.arab
        audio.play() // 自动播放
    }

    return (
        <div className="container">
            <div className="player">
                <div className="ayah" ref={ayahRef}>
                    إِضغطْ عَلى السُّورة للإستماعِ إليها
                </div>
                <audio className="quranPlayer" controls autoPlay ref={audioRef}></audio>
                <div className="buttons">
                    <div className="icons next" onClick={() => setAyahIndex(prev => Math.max(0, prev - 1))}>⏮️</div>
                    <div className="icons play" onClick={() => audioRef.current?.play()}>▶️</div>
                    <div className="icons prev" onClick={() => setAyahIndex(prev => Math.min(ayahsAudios.length - 1, prev + 1))}>⏭️</div>
                </div>
            </div>

            <div className="surahs">
                {surahs.map((data, index) => (
                    <div key={index} onClick={() => handleSurah(index)}>
                        <p>{data.name.long}</p>
                        <p>{data.name.transliteration.en}</p>
                    </div>
                ))}
            </div>
        </div>
    )
}

export default QuranPlayer

样式代码(无修改)

.container {
    max-width: 1200px;
    width: 100%;
    padding: 0 15px;
    margin: 0 auto;

    .player {
        background-image: linear-gradient(to right top, #2e3b4a, #2c3846, #2b3543, #29333f, #27303c, #283543, #293b49, #294050, #285060, #26616e, #277379, #31847f);
        top: 0;
        padding: 20px;
        margin: 10px 0;
        border-radius: 5px;
        position: sticky !important;
        z-index: 2;

        .ayah {
            padding: 10px 0;
            font-size: 2rem;
            color: #fff;
            text-align: center;
        }

        audio {
            width: 100%;
            height: 50px;
            margin: 15px 0;
        }

        .buttons {
            display: flex;
            justify-content: space-around;

            .icons {
                width: 60px;
                height: 60px;
                border-radius: 50%;
                background-color: #fff;
                display: grid;
                place-items: center;
                color: #313744;
                font-size: 1.1rem;
                cursor: pointer;
            }
        }
    }

    .surahs {
        div {
            color: #313744;
            background-color: #fff;
            box-shadow: 0 0 4px rgba(0, 0, 0, .8);
            font-weight: 500;
            font-size: 1.2rem;
            width: calc(100% - 90px);
            transition: .6s color ease-in-out;
            cursor: pointer;
            margin-bottom: 10px;
            padding: 10px 20px;
            position: relative;
            counter-increment: surah-counter;

            &::after {
                content: counter(surah-counter);
                position: absolute;
                height: 80px;
                width: 80px;
                left: -90px;
                top: 50%;
                transform: translateY(-50%);
                border-radius: 50%;
                font-size: inherit;
                line-height: 80px;
                text-align: center;
                transition: inherit;
                background-color: inherit;
                box-shadow: inherit;

                &:hover {
                    color: #fff;
                    background-color: #2e3b4a;
                }
            }

            &::before {
                content: "";
                position: absolute;
                border: 7px solid transparent;
                border-right-color: #2e3b4a;
                top: 50%;
                transform: translateY(-50%);
                left: -14px;
            }

            &:hover {
                color: #fff;
                background-color: #2e3b4a;
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者WaLid LamRaoui

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 03:35:03