React数组修改后无法渲染问题求助
打字动画React组件渲染异常问题排查与修复
问题描述
开发了一个React组件,接收字符串数组,将字符包裹在span标签中并切换opacity实现打字动画。发现修改displayedText数组后无法正常渲染,交换displayedText[0]和[1]后,渲染情况反转。
原问题代码
"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { useInterval } from "src/hooks/setInterval"; type Props = { text: string[]; aniLength: number }; export default function TypeIn({ text, aniLength }: Props) { const fullText = text.join(""); const [numPrintedLetter, setNumPrintedLetter] = useState<number>(0); const [numPrintedStrings, setNumPrintedStrings] = useState<number>(0); const [delay, setDelay] = useState<number | null>( aniLength / fullText.length ); const [displayedText, setDisplayedText] = useState<JSX.Element[][]>(() => text.map((stringArr) => stringArr.split("").map((letter, i) => ( <span key={i} style={{ opacity: "0" }}> {letter} </span> )) ) ); useEffect(() => { if (text.length <= 0) throw Error("CAN NOT TYPE IN NOTHING"); }, [text]); useInterval(() => { if (numPrintedLetter < text[numPrintedStrings].length) { // Here is the place where code per character is executed setDisplayedText((prevText) => { let curNewElement = ( <span key={prevText[numPrintedStrings][numPrintedLetter].key} style={{ opacity: "1.0" }} > {prevText[numPrintedStrings][numPrintedLetter].props.children} </span> ); prevText[numPrintedStrings][numPrintedLetter] = curNewElement; return prevText; }); setNumPrintedLetter(numPrintedLetter + 1); } else { // subtract one to compensate for 0 index if (numPrintedStrings >= text.length - 1) { setDelay(null); } else { setNumPrintedLetter(0); setNumPrintedStrings(numPrintedStrings + 1); } } }, delay); useEffect(() => { console.log("displayedText", displayedText); }, [displayedText]); return ( <> <h1 className={`text-stroke-2 pt-[max(76px,32vh)]`}> {displayedText[0]} <Link href="/contact" className="text-transparent dark:text-stroke-white text-stroke-black text-stroke-2 dark:hover:text-white hover:text-black transition-all ease-in-out duration-300" > {displayedText[1]} </Link> </h1> </> ); }
问题根源
- 直接修改原状态数组:在
setDisplayedText回调中直接修改prevText数组元素,React无法检测到引用变化,不会触发重新渲染。 - 将JSX元素存入状态:把UI元素存在state里是React反模式,状态应仅存储数据,UI渲染逻辑需放在组件render阶段。
- 闭包陷阱:
useInterval中依赖的numPrintedLetter和numPrintedStrings可能因闭包无法获取最新值,导致逻辑异常。
修复方案
1. 重构状态,仅存储数据
将状态改为记录每个字符串已打印的字符数量,而非JSX元素:
const [printedCounts, setPrintedCounts] = useState<number[]>( () => new Array(text.length).fill(0) );
2. 动态生成UI元素
渲染时根据printedCounts动态生成带opacity的span:
const renderTextSegment = (index: number) => { const str = text[index]; const printedCount = printedCounts[index]; return str.split("").map((letter, i) => ( <span key={i} style={{ opacity: i < printedCount ? 1 : 0 }}> {letter} </span> )); };
3. 修复状态更新逻辑
使用函数式更新确保获取最新状态,避免闭包问题:
useInterval(() => { setPrintedCounts((prevCounts) => { const newCounts = [...prevCounts]; if (newCounts[numPrintedStrings] < text[numPrintedStrings].length) { newCounts[numPrintedStrings] += 1; setNumPrintedLetter((prev) => prev + 1); } else { if (numPrintedStrings >= text.length - 1) { setDelay(null); } else { setNumPrintedLetter(0); setNumPrintedStrings((prev) => prev + 1); } } return newCounts; }); }, delay);
修复后的完整代码
"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { useInterval } from "src/hooks/setInterval"; type Props = { text: string[]; aniLength: number }; export default function TypeIn({ text, aniLength }: Props) { const fullText = text.join(""); const [numPrintedLetter, setNumPrintedLetter] = useState<number>(0); const [numPrintedStrings, setNumPrintedStrings] = useState<number>(0); const [delay, setDelay] = useState<number | null>( aniLength / fullText.length ); // 仅存储每个字符串已打印的字符数量 const [printedCounts, setPrintedCounts] = useState<number[]>( () => new Array(text.length).fill(0) ); useEffect(() => { if (text.length <= 0) throw Error("CAN NOT TYPE IN NOTHING"); // 当text或aniLength变化时重置状态 setPrintedCounts(new Array(text.length).fill(0)); setNumPrintedLetter(0); setNumPrintedStrings(0); setDelay(aniLength / text.join("").length); }, [text, aniLength]); useInterval(() => { setPrintedCounts((prevCounts) => { const newCounts = [...prevCounts]; // 当前字符串还有未打印字符 if (newCounts[numPrintedStrings] < text[numPrintedStrings].length) { newCounts[numPrintedStrings] += 1; setNumPrintedLetter((prev) => prev + 1); } else { // 所有字符串打印完成 if (numPrintedStrings >= text.length - 1) { setDelay(null); } else { // 切换到下一个字符串 setNumPrintedLetter(0); setNumPrintedStrings((prev) => prev + 1); } } return newCounts; }); }, delay); // 根据打印进度动态生成UI const renderTextSegment = (index: number) => { const str = text[index]; const printedCount = printedCounts[index]; return str.split("").map((letter, i) => ( <span key={i} style={{ opacity: i < printedCount ? 1 : 0 }}> {letter} </span> )); }; return ( <> <h1 className={`text-stroke-2 pt-[max(76px,32vh)]`}> {renderTextSegment(0)} <Link href="/contact" className="text-transparent dark:text-stroke-white text-stroke-black text-stroke-2 dark:hover:text-white hover:text-black transition-all ease-in-out duration-300" > {renderTextSegment(1)} </Link> </h1> </> ); }
关键优化点
- 状态与UI分离:状态仅存数据,UI由数据驱动,符合React设计理念。
- 不可变更新:每次状态更新都创建新数组,确保React能检测到变化并触发渲染。
- 避免闭包陷阱:使用函数式更新获取最新状态值,保证逻辑正确性。
内容的提问来源于stack exchange,提问作者Laycookie
相关产品推荐
相关产品推荐

