反应 useState 不更新
React useState not updating
我正在学习 React,我知道很多问题都涉及到这个主题,但它们都集中在 useState
的异步性质上。我不确定这是否是这里发生的事情。我也试了一个结合useEffect
的版本,结果还是一样
我有一个组件,我正在听按键 - 用户试图猜一个词。猜出单词后,word
对象应该被替换为另一个对象,新的拼图开始。
组件使用正确的状态(新单词的字符)进行渲染,但是当尝试第一次猜测第二个谜题时,word
对象仍然是原始状态对象。
如何正确更新此 word
对象?
在 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”并找到更多好的文章。
我正在学习 React,我知道很多问题都涉及到这个主题,但它们都集中在 useState
的异步性质上。我不确定这是否是这里发生的事情。我也试了一个结合useEffect
的版本,结果还是一样
我有一个组件,我正在听按键 - 用户试图猜一个词。猜出单词后,word
对象应该被替换为另一个对象,新的拼图开始。
组件使用正确的状态(新单词的字符)进行渲染,但是当尝试第一次猜测第二个谜题时,word
对象仍然是原始状态对象。
如何正确更新此 word
对象?
在 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”并找到更多好的文章。