reactjs - React useState wird nicht aktualisiert
Ich lerne React und ich weiß, dass dieses Thema durch viele Fragen abgedeckt wurde, aber alle konzentrieren sich auf die asynchrone Natur von useState. Ich bin mir nicht sicher, ob das hier der Fall ist. Ich habe auch eine Version in Kombination mit useEffect ausprobiert, und das Ergebnis war dasselbe.
Ich habe eine Komponente, in der ich auf Tastendrücke höre - der Benutzer versucht, ein Wort zu erraten. Sobald das Wort erraten wurde, soll das word-Objekt durch ein anderes ersetzt werden und ein neues Rätsel beginnt.
Die Komponente rendert unter Verwendung des richtigen Zustands (Buchstaben des neuen Wortes), aber beim Versuch des ersten Versuchs des zweiten Puzzles ist das word-Objekt immer noch das ursprüngliche Zustandsobjekt.
Wie kann ich dieses word-Objekt korrekt aktualisieren?
CodeSandbox
Schritte zur Reproduktion in der 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;
Wort.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;
Antwort #1
Das Problem ist, dass keyDownHandler word vom initialen Rendern erfasst und dann nur einmal an keydown angehängt wird. Sie müssen keyDownHandler jedes Mal entfernen und erneut anhängen, wenn sich word ändert (denken Sie daran, dass React bei jedem Aufruf von setWord ein Re-Render auslöst, bei dem word den neuen Wortwert enthält).
Hier ist die Lösung:
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
// ...
};
Jetzt wird jedes Mal, wenn WordFrame mit einem neuen word neu gerendert wird, eine frische Kopie des keyDownHandler-Closures angehängt, das den aktuellsten Wert von word erfasst.
Wenn Sie mehr über diesen Mechanismus lesen möchten, empfehle ich Ihnen wärmstens Dan Abramovs (umfassenden) Leitfaden hier. Sie können auch "stale closure React useEffect" googeln und einige weitere gute Artikel finden.
Antwort #2
Es ist schwer ohne einen Codesandbox zu debuggen, aber ich vermute, da Sie möchten, dass der useEffect ausgelöst wird, wenn Sie es tun
setRenderedCharacters([...word.renderedCharacters]);Sie sollten renderedCharacters als Abhängigkeit hinzufügen
useEffect(() => {
if (word.isGuessed) {
const newWord = getWord();
setWord(newWord);
setRenderedCharacters(newWord.renderedCharacters);
}
}, [word.isGuessed, renderedCharacters]);
