React实现类Wordle应用按回车时State未正确更新问题
使用React开发类Wordle小游戏时,用字符数组state存储当前输入的字母,用数组state存储已提交的尝试内容,通过全局键盘事件监听输入:
- 输入字母时,通过函数式更新累加
currentAttempt数组,页面渲染正常
- 按下回车键提交时,控制台打印的
currentAttempt为空,最终存入storedAttempts的内容为空
问题相关核心代码如下:
import { useEffect, useState } from "react"; const Board = () => { const [currentAttempt, setCurrentAttempt] = useState([]); const [storedAttempts, setStoredAttempts] = useState([]); useEffect(() => { const onLetterTyped = (e) => { // Accept only letters if (e.keyCode >= 65 && e.keyCode <= 90) { setCurrentAttempt((prevState) => [...prevState, e.key]); } // Enter if (e.keyCode === 13) { console.log("Current attempt: " + currentAttempt); setStoredAttempts((prevState) => [ ...prevState, currentAttempt.join(""), ]); setCurrentAttempt([]); } }; window.addEventListener("keyup", onLetterTyped); return () => { window.removeEventListener("keyup", onLetterTyped); }; }, []); return ( <div> <p>{currentAttempt}</p> <div> {storedAttempts.map((word, i) => ( <div key={i}>{word}</div> ))} </div> </div> ); }; export default Board;
这是React Hooks使用时非常典型的**陈旧闭包(Stale Closure)**问题:
- 你给
useEffect传入的依赖数组是空数组[],代表这个副作用逻辑只会在组件首次挂载时执行一次,后续不会重新执行。 - 首次执行时绑定的
onLetterTyped键盘事件回调函数,闭包内捕获的currentAttempt永远是组件初次渲染时的初始值——也就是空数组[]。
输入字母时页面能正常渲染,是因为字母输入逻辑用了函数式更新:setCurrentAttempt((prevState) => [...prevState, e.key]),这种写法不需要依赖外部闭包捕获的currentAttempt值,React会自动传入最新的前序状态,所以能正常累加字符。
但按下回车的逻辑里,直接读取了闭包内捕获的currentAttempt,这个值从组件挂载开始就没有更新过,永远是空数组,所以控制台打印为空,拼接后存入storedAttempts的也会是空字符串。
可以任选以下两种方案修复:
方案1:将状态加入useEffect依赖数组
把currentAttempt加入useEffect的依赖列表,每次currentAttempt变化时,就会重新执行副作用:解绑旧的事件回调,绑定能捕获到最新状态值的新回调。
修改后的useEffect代码如下:
useEffect(() => { const onLetterTyped = (e) => { // 仅接受字母输入 if (e.keyCode >= 65 && e.keyCode <= 90) { setCurrentAttempt((prevState) => [...prevState, e.key]); } // 回车键提交逻辑 if (e.keyCode === 13) { console.log("Current attempt: " + currentAttempt); setStoredAttempts((prevState) => [ ...prevState, currentAttempt.join(""), ]); setCurrentAttempt([]); } }; window.addEventListener("keyup", onLetterTyped); return () => { window.removeEventListener("keyup", onLetterTyped); }; }, [currentAttempt]); // 将currentAttempt加入依赖
这种写法符合React Hooks的常规设计逻辑,可读性高,适合绝大多数场景。
方案2:用Ref同步最新状态,避免事件重复绑定
如果不想每次输入字母都重新绑定键盘事件,可以借助useRef存储最新的状态值,让事件回调随时能读到最新的currentAttempt,不需要修改useEffect的空依赖:
import { useEffect, useState, useRef } from "react"; const Board = () => { const [currentAttempt, setCurrentAttempt] = useState([]); const [storedAttempts, setStoredAttempts] = useState([]); // 用ref同步跟踪currentAttempt的最新值 const currentAttemptRef = useRef(currentAttempt); currentAttemptRef.current = currentAttempt; useEffect(() => { const onLetterTyped = (e) => { if (e.keyCode >= 65 && e.keyCode <= 90) { setCurrentAttempt((prevState) => [...prevState, e.key]); } if (e.keyCode === 13) { const latestVal = currentAttemptRef.current; console.log("Current attempt: " + latestVal); setStoredAttempts((prevState) => [ ...prevState, latestVal.join(""), ]); setCurrentAttempt([]); } }; window.addEventListener("keyup", onLetterTyped); return () => { window.removeEventListener("keyup", onLetterTyped); }; }, []); // 保持空依赖,事件只在挂载时绑定一次 return ( <div> <p>{currentAttempt}</p> <div> {storedAttempts.map((word, i) => ( <div key={i}>{word}</div> ))} </div> </div> ); }; export default Board;
这种方案性能更好,适合事件绑定逻辑较重、状态更新频繁的场景。Wordle类游戏输入频率低,两种方案实际体验没有可感知的差异。
内容的提问来源于stack exchange,提问作者vtomic85

