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

React中useRef无法获取动态添加字母DOM元素的问题求解

解决方案

问题根源

你遇到的核心问题是React状态更新的异步性:调用setWords添加新字母后,组件不会立即重新渲染,新的<span>元素还未挂载到DOM,此时activeLetter.current仍为null。另外,直接通过ref操作DOM样式的方式不符合React状态驱动的设计理念,容易引发DOM与状态不同步的问题。

推荐方案:状态驱动样式(无需ref)

放弃直接操作DOM,改用状态跟踪每个字母的输入状态,渲染时根据状态自动应用样式。这种方式完全贴合React的设计思想,能彻底解决动态元素的样式同步问题。

代码示例

import { useState, useEffect } from 'react';
import styles from './your-styles.module.css'; // 替换为你的样式文件路径

export default function Home() {
  const [words, setWords] = useState([
    "these", "are", "the", "words", "you", "are", "typing"
  ]);
  const [currWordIdx, setCurrWordIdx] = useState(0);
  const [currLetterIdx, setCurrLetterIdx] = useState(0);
  // 跟踪每个字母的状态:default | correct | wrong | wrong-extra
  const [letterStates, setLetterStates] = useState(() => {
    return words.map(word => Array(word.length).fill('default'));
  });

  useEffect(() => {
    const handleKeyDown = (e) => {
      // 处理退格键(可根据需求扩展逻辑)
      if (e.key === 'Backspace') {
        e.preventDefault();
        if (currLetterIdx > 0) {
          setCurrLetterIdx(prev => prev - 1);
        } else if (currWordIdx > 0) {
          setCurrWordIdx(prev => prev - 1);
          setCurrLetterIdx(words[currWordIdx - 1].length);
        }
        return;
      }

      // 处理空格:切换到下一个单词
      if (e.key === ' ') {
        if (currLetterIdx >= words[currWordIdx].length) {
          setCurrWordIdx(prev => Math.min(prev + 1, words.length - 1));
          setCurrLetterIdx(0);
        }
        e.preventDefault();
        return;
      }

      const currentWord = words[currWordIdx];
      let newWords = [...words];
      let newLetterStates = [...letterStates];

      // 额外输入场景:当前单词已输完,且输入非空格/退格
      if (currLetterIdx === currentWord.length) {
        newWords[currWordIdx] = currentWord + e.key;
        newLetterStates[currWordIdx] = [...newLetterStates[currWordIdx], 'wrong-extra'];
        setWords(newWords);
      } else {
        // 正常输入:判断输入是否正确
        const isCorrect = e.key === currentWord[currLetterIdx];
        newLetterStates[currWordIdx][currLetterIdx] = isCorrect ? 'correct' : 'wrong';
      }

      setLetterStates(newLetterStates);
      setCurrLetterIdx(prev => prev + 1);
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [currWordIdx, currLetterIdx, words, letterStates]);

  return (
    <div>
      <div className={styles.container}>
        {words.map((word, wordIdx) => (
          <div key={wordIdx} className={wordIdx === currWordIdx ? styles.activeWord : ''}>
            {word.split('').map((letter, letterIdx) => {
              const state = letterStates[wordIdx]?.[letterIdx] || 'default';
              let spanClass = styles.defaultLetter;
              
              switch(state) {
                case 'correct': spanClass = styles.right; break;
                case 'wrong': spanClass = styles.wrong; break;
                case 'wrong-extra': spanClass = styles.wrongextra; break;
              }

              // 标记当前活跃字母(可选,比如添加下划线)
              if (wordIdx === currWordIdx && letterIdx === currLetterIdx) {
                spanClass += ` ${styles.activeLetter}`;
              }

              return (
                <span key={letterIdx} className={spanClass}>
                  {letter}
                </span>
              );
            })}
          </div>
        ))}
      </div>
    </div>
  );
}

方案优势

  1. 状态与视图完全同步:样式由letterStates状态控制,无需直接操作DOM,彻底避免ref异步问题。
  2. 逻辑清晰可扩展:新增输入状态(如当前字母高亮)只需修改状态逻辑,无需调整DOM操作代码。
  3. 符合React设计规范:遵循数据驱动视图的核心思想,减少潜在bug。

备选方案:ref结合useEffect监听DOM更新

若坚持使用ref思路,可通过useEffect监听状态变化,等待组件重新渲染、DOM更新完成后再修改样式。

代码示例

import { useState, useEffect, useRef } from 'react';
import styles from './your-styles.module.css';

export default function Home() {
  const [words, setWords] = useState([
    "these", "are", "the", "words", "you", "are", "typing"
  ]);
  const [currword, setCurrword] = useState(0);
  const [currletter, setCurrletter] = useState(0);

  const activeLetter = useRef<HTMLSpanElement>(null);
  const isExtraInput = useRef(false); // 标记是否为额外输入场景

  useEffect(() => {
    const handleKeyDown = (e) => {
      if (words[currword].length === currletter && e.key !== ' ' && e.key !== 'Backspace') {
        const wordsCopy = [...words];
        wordsCopy[currword] += e.key;
        setWords(wordsCopy);
        isExtraInput.current = true;
        setCurrletter(currletter + 1);
      } else {
        isExtraInput.current = false;
        const isCorrect = e.key === words[currword][currletter];
        if (activeLetter.current) {
          activeLetter.current.className = isCorrect ? styles.right : styles.wrong;
        }
        setCurrletter(currletter + 1);
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [currletter, currword, words]);

  // 监听状态更新,DOM渲染完成后处理额外输入的样式
  useEffect(() => {
    if (isExtraInput.current && activeLetter.current) {
      activeLetter.current.className = styles.wrongextra;
      isExtraInput.current = false; // 重置标记
    }
  }, [words, currletter]);

  return (
    <div>
      <div className={styles.container}>
        {words.map((word, widx) => (
          <div key={widx}>
            {word.split('').map((letter, lidx) => (
              <span
                key={lidx}
                ref={lidx === currletter && widx === currword ? activeLetter : null}
              >
                {letter}
              </span>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

方案说明

  • 使用isExtraInput ref标记额外输入场景。
  • 第二个useEffect监听words和currletter的变化,确保DOM更新完成后再获取activeLetter.current修改样式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 02:50:34