React中radio按钮键盘/鼠标选中时事件参数获取异常问题
问题分析
当前代码存在两个核心问题:
- 同一个
handleChange函数同时处理单选框的onChange事件和全局keydown事件,但两种事件的event对象结构完全不同:- onChange事件的event是InputEvent,
e.target指向触发事件的单选框,包含value、name等关键属性; - keydown事件的event是KeyboardEvent,
e.target可能是页面任意元素,无法直接获取单选框的value和name,导致keydown时无法正确更新答题状态。
- onChange事件的event是InputEvent,
- 单选框的
checked状态仅依赖keyCode状态,无法和实际选中值关联,且全局监听keydown会导致所有页面按键都触发逻辑,干扰其他操作。
解决方案
我们需要将键盘事件和鼠标事件的处理逻辑拆分,同时确保键盘操作能精准定位到当前显示的题目,并触发对应的单选框选中和状态更新。
步骤1:拆分事件处理函数
将原handleChange拆分为两个独立函数:
handleRadioChange:处理单选框的点击/选中事件,负责更新答题状态;handleKeyDown:处理键盘事件,根据按键(A/B/C/D)找到当前题目对应的单选框,手动触发其选中状态并调用handleRadioChange。
步骤2:优化状态管理
- 移除
keyCode状态,改用answersSelected来控制单选框的checked状态,确保状态和实际选中值一致; - 修复
index1的使用问题(原代码中var index1 = 0在map中会导致索引混乱,改用map的第二个参数作为题目索引)。
步骤3:精准绑定键盘事件
仅在当前题目容器上绑定键盘事件,避免全局监听的干扰,同时确保键盘操作只作用于当前显示的题目。
修改后的完整代码
import { useState, useEffect, useRef } from "react"; import { useLocation } from "react-router-dom"; import { englishQuestions } from "../questions/englishQ"; const English = () => { const { state } = useLocation(); const { user } = state; // 答题相关状态 const [input, setInputs] = useState<{ questionNumber: string; question: string; answer: string; status: "correct" | "wrong"; marks: string; }[]>([]); const [answersSelected, setAnswersSelected] = useState<Record<string, string>>({}); const [selected, setSelected] = useState<Record<string, boolean>>({}); const [submitted, setSubmitted] = useState(false); const [allSelected, setAllSelected] = useState(false); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(1); const answers = englishQuestions.map(item => item.answer); const numbersRegex = /\d+/g; // 处理单选框选中事件(鼠标/键盘触发) const handleRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { value, name } = e.target; // 解析题目编号和索引 const currentQuestionNumber = name.slice(name.indexOf("englishQ"), 10); const currentQuestion = name.slice(name.indexOf(":") + 2); const currentArrayIndex = parseInt(name.match(numbersRegex)![0]) - 1; // 更新答题记录 const isCorrect = answers[currentArrayIndex] === value; setInputs(prev => { // 先移除该题已有的记录(避免重复添加) const filtered = prev.filter(item => item.questionNumber !== currentQuestionNumber.trim()); return [ ...filtered, { questionNumber: currentQuestionNumber.trim(), question, answer: value, status: isCorrect ? "correct" : "wrong", marks: isCorrect ? "2" : "0", } ]; }); setAnswersSelected(prev => ({ ...prev, [name]: value })); setSelected(prev => ({ ...prev, [currentArrayIndex + 1]: true })); // 检查是否所有题目都已作答 const answeredCount = Object.values(selected).filter(Boolean).length + 1; setAllSelected(answeredCount >= 10); // 更新本地存储 const selectedValues = Object.values(answersSelected).map(val => ({ theSelectedValue: val })); localStorage.setItem("theSelectedValue", JSON.stringify(selectedValues)); }; // 处理键盘按键事件 const handleKeyDown = (e: KeyboardEvent) => { if (submitted) return; // 已提交则不响应键盘操作 const key = e.code.toUpperCase(); let targetOptionId = ""; switch(key) { case "KEYA": targetOptionId = `q${currentQuestionIndex}-a`; break; case "KEYB": targetOptionId = `q${currentQuestionIndex}-b`; break; case "KEYC": targetOptionId = `q${currentQuestionIndex}-c`; break; case "KEYD": targetOptionId = `q${currentQuestionIndex}-d`; break; default: return; // 非A/B/C/D按键不处理 } // 找到对应的单选框并触发选中 const targetRadio = document.getElementById(targetOptionId) as HTMLInputElement; if (targetRadio && !targetRadio.disabled) { targetRadio.checked = true; handleRadioChange({ target: targetRadio } as React.ChangeEvent<HTMLInputElement>); } }; const submit = (e: React.FormEvent) => { e.preventDefault(); setSubmitted(true); localStorage.setItem("studentId", JSON.stringify(user.studentId)); localStorage.setItem("englishSubmitted", "true"); localStorage.setItem("englishAnswers", JSON.stringify(input)); }; // 绑定/解绑键盘事件 useEffect(() => { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [currentQuestionIndex, submitted, answersSelected]); return ( <> <div className="bg-gray-200 mx-auto my-10 w-3/4 p-10 shadow-sm shadow-gray-600 rounded-lg"> <form onSubmit={submit} className="mb-10"> {englishQuestions.map((question, idx) => { const questionNumber = idx + 1; const isCurrent = questionNumber === currentQuestionIndex; return ( <div key={questionNumber} className={isCurrent ? "" : "hidden"} > <h3> <span className={submitted ? "text-gray-400" : "font-bold"}> Q {questionNumber}: </span>{" "} {question.start}{" "} <span className="underline">{question.theWord}</span>{" "} {question.end} </h3> {Object.entries(question.options[0]).map(([optionKey, optionValue]) => { const radioName = `englishQ${questionNumber} : ${question.start} ${question.theWord} ${question.end}`; const radioId = `q${questionNumber}-${optionKey.toLowerCase()}`; return ( <div className="mb-3" key={optionKey}> <label htmlFor={radioId}>{optionKey}: </label> <input id={radioId} type="radio" name={radioName} value={optionValue} onChange={handleRadioChange} disabled={submitted} checked={answersSelected[radioName] === optionValue} /> <label htmlFor={radioId}> {optionValue}</label> <br /> </div> ); })} </div> ); })} <div className="flex justify-center"> <button type="submit" name="submit" className="btnExam p-3 mx-auto cursor-pointer" disabled={!allSelected} // 未答完所有题则禁用提交按钮 > Submit </button> </div> </form> </div> </> ); }; export default English;
关键修改点说明
拆分事件处理:
handleRadioChange专注于处理单选框的选中逻辑,统一处理鼠标和键盘触发的选中操作;handleKeyDown仅负责解析按键,找到对应的单选框并触发handleRadioChange,避免直接处理状态更新逻辑。
状态关联优化:
- 单选框的
checked状态绑定到answersSelected,确保选中状态和实际存储的答案一致,不再依赖不稳定的keyCode; - 修复
input状态重复添加的问题,更新答题记录时先移除该题已有记录,避免数组冗余。
- 单选框的
键盘事件精准化:
- 根据当前显示的题目索引生成单选框ID,确保键盘操作只作用于当前题目;
- 已提交状态下禁用键盘操作,避免无效交互。
代码健壮性提升:
- 修复原代码中未定义的
allSelected状态; - 使用map的索引替代全局
index1,避免索引混乱; - 为单选框添加唯一ID,解决原代码中多个单选框ID重复的问题(原代码所有题目的A选项ID都是"a",会导致label关联错误)。
- 修复原代码中未定义的
内容的提问来源于stack exchange,提问作者Misbahu Abubakar
相关产品推荐
相关产品推荐

