javascript - สถานะของ React ไม่อัปเดตเมื่อเกิดเหตุการณ์คีย์
ฉันกำลังเจอปัญหาที่ไม่สามารถหาคำตอบได้ ฉันกำลังสร้างเกมเลียนแบบ Wordle อยู่ สถานะดูเหมือนจะอัปเดตในบางเหตุการณ์แต่ไม่ใช่ทุกเหตุการณ์ และฉันก็ไม่สามารถหาสาเหตุได้
ฉันมีคอมโพเนนต์ Keyboard ซึ่งรับ handleKeyClick เป็นพรอพจากคอมโพเนนต์ต้นทาง และถูกแนบเข้ากับตัวจัดการเหตุการณ์สองตัว
ส่วนประกอบหลัก
import { Box, Divider, Grid, Typography } from "@mui/material";
import { useState, useEffect, useCallback } from 'react';
import Keyboard from "../Keyboard";
import { v4 as uuid } from 'uuid'
import WordleNotifbar from "../WordleNotifBar";
import Loading from "../Utils/Loading";
import { IGuessState } from "../../types";
interface IGuessGridProps {
addGuess: Function,
guesses: any,
answer: any
}
const GuessGrid = (props: IGuessGridProps) => {
const { addGuess, guesses, answer } = props;
let [notif, setNotif] = useState<boolean>(false);
const [guess, setGuess] = useState<string[]>([]);
const styles = {
input: {
border: ".5px solid white",
height: "50px",
display: "flex",
borderRadius: "5px",
justifyContent: "center",
alignItems: "center",
backgroundColor: "",
color: "white",
},
container: {
minWidth: "300px",
width: "30%",
maxWidth: "450px",
margin: "0 auto",
marginTop: "15px",
},
}
// In the parent component, I have defined the function I'm passing in as a prop as such:
const handleAddCharacter = (char: string) => {
setGuess([...guess, char])
}
// Not fully implemented yet
const handleBackspace = (e: MouseEvent): void => {
e.preventDefault();
setGuess([...guess])
}
const handleSubmit = (): void => {
let word = guess.join('')
if (word.length === answer.length) {
addGuess(word.toLowerCase())
setGuess([]);
}
else {
setNotif(true);
setTimeout(() => {
setNotif(false);
}, 1000)
}
}
if (answer) {
return <>
<Divider />
<Grid container sx={styles.container} >
{answer.split('').map((_: string, index: number) => {
return (<Grid item xs={12 / answer.length} sx={styles.input} key={uuid()}>
<Box>
<Typography>
{guess[index]}
</Typography>
</Box>
</Grid>)
})}
</Grid>
<Keyboard guesses={guesses} answer={answer} handleKeyClick={handleAddCharacter} handleBackspace={handleBackspace} submitFunc={handleSubmit} />
{notif ? <WordleNotifbar message="Not Enough Characters" duration={1000} /> : ""}
</>;
} else {
return <Loading />
}
};
export default GuessGrid;
ส่วนประกอบคีย์บอร์ด
import { Box, Grid, SxProps, Theme, Typography } from "@mui/material";
import { useCallback, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
import BackspaceIcon from '@mui/icons-material/Backspace';
import React from "react";
interface IKeyboardProps {
guesses: string[],
answer: string,
handleKeyClick: any,
submitFunc: any,
handleBackspace: any
}
const Keyboard = (props: IKeyboardProps) => {
const { guesses, answer, handleKeyClick, submitFunc, handleBackspace } = props
const [guessedLetters, setGuessedLetters] = useState<string[]>();
const topRow = 'qwertyuiop'.toUpperCase().split('');
const middleRow = 'asdfghjkl'.toUpperCase().split('');
const bottomRow = 'zxcvbnm'.toUpperCase().split('');
const allKeys = topRow.concat(middleRow.concat(bottomRow));
// When the component is initialized, I am establishing an event listener in the window for the key press events.
useEffect(() => {
window.addEventListener('keypress', handlePhysicalKeyPress)
}, [])
useEffect(() => {
const allGuessedCharacters = guesses.join('').split('');
const uniqueGuessedCharacters = allGuessedCharacters.filter((val: string, index: number, self) => self.indexOf(val) === index)
setGuessedLetters(uniqueGuessedCharacters);
}, [guesses])
const handleVirtualKeyPress = (e: any) => {
handleKeyClick(e.target.textContent)
}
const handlePhysicalKeyPress = (e: KeyboardEvent) => {
e.preventDefault()
if (allKeys.includes(e.key.toUpperCase())) {
handleKeyClick(e.key.toUpperCase());
}
}
const genKeyStyles = (character: string, _: number): SxProps<Theme> => {
character = character.toLowerCase()
const styles = {
width: character === "bs" || character === "enter" ? "63px" : "33px",
marginX: "1px",
marginY: "1px",
borderRadius: "5px",
height: "50px",
color: "black",
textAlign: "center",
backgroundColor: "#DDD",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
if (guessedLetters) {
if (answer.indexOf(character) >= 0 && guessedLetters.indexOf(character) >= 0) {
styles.backgroundColor = "green"
} else if (answer.indexOf(character) < 0 && guessedLetters.indexOf(character) >= 0) {
styles.backgroundColor = "#777"
}
}
return styles
}
return <Box sx={{ display: "flex", flexDirection: "column", justifyContent: "center", marginTop: "10px", }}>
<Box sx={{ display: "flex", justifyContent: "center" }}>
{topRow.map((letter: string, index: any) => {
return (
<Box sx={genKeyStyles(letter, index)} key={uuid()} onClick={handleVirtualKeyPress}>
<Typography key={uuid()}>{letter}</Typography>
</Box>
)
})}
</Box>
<Box sx={{ display: "flex", justifyContent: "center" }}>
{middleRow.map((letter: string, index: any) => {
return (
<Box sx={genKeyStyles(letter, index)} key={uuid()} onClick={handleVirtualKeyPress}>
<Typography key={uuid()}>{letter}</Typography>
</Box>
)
})}
</Box>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Box sx={genKeyStyles("enter", 1)} key={uuid()} onClick={submitFunc}>
<Typography key={uuid()}>enter</Typography>
</Box>
{bottomRow.map((letter: string, index: any) => {
return (
<Box sx={genKeyStyles(letter, index)} key={uuid()} onClick={handleVirtualKeyPress}>
<Typography key={uuid()}>{letter}</Typography>
</Box>
)
})}
<Box sx={genKeyStyles("bs", 1)} key={uuid()} onClick={handleBackspace}>
<Typography key={uuid()}><BackspaceIcon /></Typography>
</Box>
</Box>
</Box>
};
export default Keyboard;
สิ่งที่เกิดขึ้นคือการกดปุ่มเสมือนดูเหมือนจะอัปเดตสถานะได้อย่างถูกต้อง แต่การกดปุ่มจริงดูเหมือนจะรีเซ็ตสถานะกลับไปเป็นอาร์เรย์ว่าง ๆ ผมไม่สามารถหาสาเหตุที่ดีว่าทำไมสิ่งนี้ถึงเกิดขึ้นได้ มีความคิดเห็นอะไรบ้างไหม? ผมขอขอบคุณล่วงหน้าสำหรับความช่วยเหลือของคุณ!
ลิงก์ไปยังแอปพลิเคชันที่ใช้งานอยู่
คำตอบ #1
เมื่อคุณทำ:
useEffect(() => {
window.addEventListener('keypress', handlePhysicalKeyPress)
}, [])
...คุณกำลังแนบฟังก์ชัน handlePhysicalKeyPress เฉพาะ เป็นตัวฟังเหตุการณ์ แต่ฟังก์ชันนั้นถูกสร้างใหม่ทุกครั้งที่คอมโพเนนต์ทำการ re-render ดังนั้นคุณจึงไม่ได้อ้างอิงถึงเวอร์ชันฟังก์ชัน "ปัจจุบัน" (ถ้าคุณพยายามลบออก คุณจะไม่สามารถทำได้เพราะมันไม่ใช่อ้างอิงเดียวกันอีกต่อไป)
ดังนั้น ผู้ฟังที่แท้จริงคือ "เวอร์ชัน" แรกสุดของฟังก์ชันของคุณ ซึ่งเรียกใช้ "เวอร์ชัน" แรกสุดของ prop handleKeyClick ของคุณ ซึ่งเป็น "เวอร์ชัน" แรกสุดของฟังก์ชัน handleAddCharacter ของคุณ ซึ่งรู้จักเพียงเวอร์ชันแรกสุดของ state guess ของคุณ... ซึ่งเป็นอาร์เรย์ว่าง
นั่นคือเหตุผลที่เมื่อ handlePhysicalKeyPress ถูกดำเนินการโดยการกดแป้นพิมพ์ มันจะสร้างอาร์เรย์การเดาใหม่จากอาร์เรย์ว่าง
ในขณะที่คุณควรหลีกเลี่ยงความแตกต่างนี้ระหว่างสิ่งที่คุณแนบไปกับผู้ฟังเหตุการณ์ของคุณและฟังก์ชัน "เวลาในการเรนเดอร์" ของคุณจริง ๆ ควรมีวิธีแก้ปัญหาอย่างง่ายสำหรับกรณีเฉพาะของคุณ: คุณควรใช้รูปแบบฟังก์ชันของตัวตั้งค่ารัฐของคุณ แม้ว่ามันจะเป็น "เวอร์ชันแรกสุด" ก็ตาม มันควรใช้เวอร์ชันรัฐ "ปัจจุบัน":
setGuess((currentGuess) => [...currentGuess, char])
