ค้นหาเว็บไซต์

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])