You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React组件绑定keydown事件后按下按键触发两次如何修复

问题修复方案

问题根因

  • 事件监听注册位置错误:你将document.addEventListener直接写在了组件顶层函数体中,React组件每次重渲染都会执行完整的函数体逻辑,相当于每次渲染都会新增一个独立的keydown事件监听,多次渲染后就会存在多个重复回调,按下按键自然会触发多次。
  • 事件清理逻辑失效:你在getKey回调中执行了removeEventListener,但每次组件渲染生成的getKey函数引用都是全新的,你只能移除当前渲染周期注册的getKey,之前渲染周期注册的旧回调无法被清理,会一直残留触发。
  • 额外的严格模式挂载:React18 开发环境默认开启严格模式,组件会执行两次挂载逻辑,如果没有正确的监听清理机制,也会导致多注册一次监听。

修复方法

将事件监听的注册和清理逻辑统一放到useEffect中,确保仅在组件挂载时注册一次,卸载时自动清理,同时修正状态直接修改的问题:

const GeneratedKey: FC<IGenerated> = (props) => {
    const [keyBoard, setKeyBoard] = useState("")
    const [arrayMovie, setArrayMovie] = useState<string[]>([])
    const idPage = props.idpage
    const nameMovie = data.results[idPage].title
    const [idLetter, setIdLetter] = useState<number>(0)
    const [indexArray, setIndexArray] = useState<number[]>([])
    const arrayNameMovie = nameMovie.split(" ").join("").split("");
    
    const getKey = (e: any) => {
        console.log("test")
        const key = e.key
        let count = 0
        for (let i = 65; i <= 90; i++) {
            if (key.toUpperCase() == String.fromCharCode(i)) {
                count = 1
            } else if (key.toLowerCase() == "backspace") {
                count = 10
            }
        }
        if (count == 1) {
            setIndexArray(prev => {
                const newIndexArray = [...prev].sort((a,b) => a - b)
                setArrayMovie(prevMovie => {
                    const newMovie = [...prevMovie]
                    newMovie.splice(newIndexArray[idLetter], 1, key)
                    return newMovie
                })
                return newIndexArray
            })
            setIdLetter(prev => prev + 1)
        } else if (count == 10) {
            if (idLetter >= 1) {
                setIdLetter(prev => prev - 1)
                setIndexArray(prev => {
                    const newIndexArray = [...prev]
                    setArrayMovie(prevMovie => {
                        const newMovie = [...prevMovie]
                        newMovie.splice(newIndexArray[idLetter - 1], 1, "")
                        return newMovie
                    })
                    return newIndexArray
                })
            }
        }
        setKeyBoard(key)
    }

    useEffect(() => {
        const initIndexArray = []
        const initArrayName = [...arrayNameMovie]
        for (let i = 3; i >= 0; i--) {
            const randomIndex = Math.floor(Math.random() * initArrayName.length)
            initArrayName.splice(randomIndex, 1, " ")
            initIndexArray.push(randomIndex)
        }
        setIndexArray(initIndexArray)
        setIdLetter(initIndexArray[0])
        setArrayMovie(initArrayName)
    }, [])

    // 统一在useEffect中管理事件监听
    useEffect(() => {
        document.addEventListener("keydown", getKey)
        // 卸载时自动清理监听
        return () => {
            document.removeEventListener("keydown", getKey)
        }
    }, [idLetter]) // 回调中用到的动态状态加入依赖数组

    return (
        <div className="down__word">
            {arrayMovie.map((letter: any, idx: number) =>
                <LettersView key={idx} letters={letter} props={undefined} />
            )}
        </div>
    )
}

额外注意事项

  • 不要直接修改useState定义的状态:原代码中直接用arrayMovie.splice、indexArray.push修改状态,属于非法的直接变更,会导致状态更新不可预期,应该通过状态更新函数的回调形式生成新的数组来修改。
  • 所有副作用(事件监听、定时器、接口请求等)都应该放到useEffect中管理,避免重复执行和内存泄漏。

内容的提问来源于stack exchange,提问作者reszi.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 14:45:04