React状态更新需两次点击问题排查与修复求助
问题:点击Surah后需两次点击才生效的原因及修复方案
问题描述
React新手开发的古兰经播放器应用,可正常从API获取数据并设置状态,但点击Surah的div触发事件更新ayahsAudios和ayahsText状态时,必须点击两次才能生效。
核心问题分析
- 状态更新异步性:React的
setState是异步操作,在handleSurah中调用setAyahsAudios(data.verses)后立刻执行changeAyah,此时ayahsAudios还未完成更新,第一次点击时使用的仍是旧状态(空数组),第二次点击时状态才更新完毕。 - DOM操作不规范:直接用
document.querySelector获取audio和ayah元素,组件渲染周期内可能无法正确获取到最新的DOM节点,导致操作失效。 - 普通变量代替状态:
ayahIndex是普通变量,组件重新渲染后会被重置为0,且无法触发视图更新;同时每次点击都绑定新的ended事件,导致事件重复触发,逻辑混乱。
修复方案
- 使用
useRef获取DOM元素,确保能稳定访问组件内的audio和ayah节点。 - 将
ayahIndex改为useState状态,让React追踪其变化并触发视图更新。 - 利用
useEffect监听ayahsAudios的变化,当状态更新完成后自动执行changeAyah逻辑。 - 用
useEffect管理audio的ended事件监听,在依赖变化时清理旧监听,避免重复绑定。 - 简化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
相关产品推荐
相关产品推荐

