反应 useState 不更新

React useState not updating

我正在学习 React,我知道很多问题都涉及到这个主题,但它们都集中在 useState 的异步性质上。我不确定这是否是这里发生的事情。我也试了一个结合useEffect的版本,结果还是一样

我有一个组件,我正在听按键 - 用户试图猜一个词。猜出单词后,word 对象应该被替换为另一个对象,新的拼图开始。

组件使用正确的状态(新单词的字符)进行渲染,但是当尝试第一次猜测第二个谜题时,word 对象仍然是原始状态对象。

如何正确更新此 word 对象?

CodeSandbox

readme.md 中重现的步骤:

const WordFrame = () => {
    const [word, setWord] = useState(() => new Word 'apple');
    const [renderedCharacters, setRenderedCharacters] = useState(
        word.renderedCharacters
    );

    const keyDownHandler = (e: KeyboardEvent): void => {
        console.log(e.key);
        if (letters.includes(e.key)) {
            const correctGuess = word.processGuess(e.key);

            if (correctGuess) {
                setRenderedCharacters([...word.renderedCharacters]);
                
                // moving the following if block into useEffect with dependency on word.isGuessed and renderedCharacters doesn't help
                if (word.isGuessed) {
                    const newWord: Word = new Word('banana');
                    setWord(newWord);
                    setRenderedCharacters(newWord.renderedCharacters);
                }
            }
        }
    };

    useEffect(() => {
        document.addEventListener('keydown', keyDownHandler);
        return () => document.removeEventListener('keydown', keyDownHandler);
    }, []);

    // useEffect(() => {
        // if (word.isGuessed) {
        //     const newWord = new Word('banana);
        //     setWord(newWord);
        //     setRenderedCharacters(newWord.renderedCharacters);
        // }
    // }, [word.isGuessed, renderedCharacters]);

    return (
        <div className='word-frame'>
            {renderedCharacters.map((c) => (
                <LetterFrame characterValue={c.value} key={c.id} />
            ))}
        </div>
    );
};

export default WordFrame;

Word.ts

class Word {
    private readonly _unguessedCharacter: string = ' ';
    private readonly _characters: string[] = [];
    private _guessingIndex = 0;

    private _renderedCharacters: Character[] = [];
    public get renderedCharacters() {
        return this._renderedCharacters;
    }

    private _isGuessed: boolean = false;
    public get isGuessed(): boolean {
        return this._isGuessed;
    }

    constructor(wordString: string) {
        console.log(`Word.constructor called with parameter: ${wordString}`);
        this._characters = wordString.split('');
        this.setRenderedCharacters();
    }

    public processGuess(letter: string): boolean {
        const isSuccessfulGuess = (): boolean =>
            this._characters[this._guessingIndex].toLowerCase() ===
            letter.toLowerCase();

        const successful = isSuccessfulGuess();

        if (successful) {
            this._guessingIndex++;
            this.setRenderedCharacters();
        }

        if (this._guessingIndex > this._characters.length - 1) {
            this._isGuessed = true;
        }

        return successful;
    }

    private setRenderedCharacters(): void {
        this._renderedCharacters = [];

        this._characters.forEach((c, i): void => {
            if (i >= this._guessingIndex) {
                this._renderedCharacters.push(
                    new Character(this._unguessedCharacter)
                );
            } else {
                this._renderedCharacters.push(new Character(c));
            }
        });
    }
}

export default Word;

没有 codesandbox 很难调试,但我猜是因为您希望在调试时触发 useEffect

setRenderedCharacters([...word.renderedCharacters]);

您应该添加 renderedCharacters 作为依赖项

useEffect(() => {
         if (word.isGuessed) {
             const newWord = getWord();
             setWord(newWord);
             setRenderedCharacters(newWord.renderedCharacters);
         }
}, [word.isGuessed, renderedCharacters]);

问题是 keyDownHandler 从初始渲染中捕获 word,然后只附加到 keydown 一次。每次 word 更改时,您都需要删除 re-attach keyDownHandler(请记住,无论何时调用 setWord,React 都会触发 re-render,其中 word包含新词值)。

修复方法如下:

const WordFrame = () => {
    const [word, setWord] = useState(() => new Word('apple'));

    // ...

    const keyDownHandler = (e: KeyboardEvent): void => { /* ... */ };

    useEffect(() => {
        document.addEventListener('keydown', keyDownHandler);
        return () => document.removeEventListener('keydown', keyDownHandler);
    }, [word]); // <-- add `word` to the dependencies for this effect

    // ...
};

现在,每次 WordFrame 使用新的 word 重新呈现时,它都会附加 keyDownHandler 闭包的新副本,该副本捕获 word 的最新值.

如果您想阅读更多关于此机制的信息,我强烈推荐 Dan Abramov 的(综合)指南 here。您还可以 Google“stale closure React useEffect”并找到更多好的文章。