React中按钮onClick事件触发两次问题求助
问题:移除按钮点击时触发两次导致删除两个输入项
我有一个用于存储组列表及每组输入项列表的对象,点击「添加新名称」或「移除」按钮可修改输入项数量。目前遇到的问题是:点击某输入项的「移除」按钮时,onClick处理函数会触发两次,导致本该删除一个输入项却删除了两个。
组件代码
import { Listbox, Transition } from "@headlessui/react"; import { CheckIcon, ChevronUpDownIcon, EllipsisVerticalIcon, } from "@heroicons/react/24/solid"; import classNames from "classnames"; import React, {Fragment, useEffect, useRef, useState} from "react"; import { useDrag, useDrop, XYCoord } from "react-dnd"; import { Controller, FieldArrayWithId, useFormState, useWatch, } from "react-hook-form"; import Trash from "../../../../components/icons/trash"; import RangeCombobox from "../../../../components/rangeCombobox"; import SlideOver from "../../../../components/side-popup"; import {PlusCircleIcon} from "@heroicons/react/24/outline"; const ManageRaceItem: React.FC<ManageRaceItemProps> = ({ index, field, checkNames, errorsArr, handleErrors, manageChartState, candidatesNamesChecked }) => { const { moveRace, removeRace, form: { control, register, unregister, watch, getValues, setValue }, } = manageChartState; const [dRaceName, setDRaceName] = useState(""); const { errors } = useFormState({ control, name: "races" }); const races = useWatch({ control, name: "races" }); const participants = useWatch({ control, name: "participants" }); const ref = useRef<HTMLDivElement>(null); const [{ handlerId }, drop] = useDrop({ accept: "CHART-RACE", collect(monitor) { return { handlerId: monitor.getHandlerId() }; }, hover(item: any, monitor) { if (!ref.current) return; const dragIndex = item.index; const hoverIndex = index; if (dragIndex === hoverIndex) return; const hoverBoundingRect = ref.current?.getBoundingClientRect(); const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; const clientOffset = monitor.getClientOffset(); const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return; if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return; moveRace(dragIndex, hoverIndex); item.index = hoverIndex; }, }); const [, drag] = useDrag(() => ({ type: "CHART-RACE", item: () => { return { id: field.id, index }; }, collect: () => ({}), })); drag(drop(ref)); const getCountAllFields = (obj: object) => { let sum = 0; for (const [key, value] of Object.entries(obj)) { if (typeof value === 'number') { sum = sum + value; } else { if(typeof value[0] === 'number') { sum = sum + value.reduce((partialSum: number, a: number) => partialSum + a, 0); } else { sum = sum + value.length; } } } return sum; } const [isHeadlessOpen, setIsHeadlessOpen] = useState(false); const [filledInputs, setFilledInputs] = useState<{[key: string]: any}>({}); const [pCandidates, setPCandidates] = useState<{[key: string]: number}>({}); return ( <> <div ref={ref} data-handler-id={handlerId} className="relative flex w-full p-5 mb-8 bg-white rounded-md shadow-md flex-col" > {/* 原有其他代码保持不变 */} <SlideOver open={isHeadlessOpen} setOpen={setIsHeadlessOpen} title="Edit Candidate Names" subtitle={`${getValues()?.races[index]?.name} Race`} > <div className="flex flex-col border-gray-300"> { [{name: 'Candidates'}].map((group, idx) => { if(pCandidates[group.name]) { return ( <> <div style={{marginBottom: '20px'}}> <span style={{marginRight: '10px'}}>{group.name.toUpperCase()}</span> <span>{`(${getCountAllFields(filledInputs?.[group.name] ?? 0)}/${pCandidates[group.name]})`}</span> <div className="flex flex-col w-[380px]"> {Array.from({ length: pCandidates[group.name] }, (_, i) => ( <div key={`name-${i}`} className="flex"> <input {...register(`races.${index}.candidateNames.${i}.name`)} type="text" placeholder="Full name" className={classNames("w-full py-3 mb-2 px-4 bg-[#E6EBF5] rounded-md", { "outline outline-2 outline-red-500 focus:outline-red-500": errors?.races?.[index]?.name, })} onBlur={(e) => { setFilledInputs(prev => { let obj = {...prev, [group.name]: prev[group.name] ? prev[group.name] : []} let arr = obj[group.name]; arr[i] = e.target.value.length > 0 ? 1 : 0; return {...obj, [group.name]: arr} }) }} /> <input {...register(`races.${index}.candidateNames.${i}.order`)} type="number" value={Number(i)} style={{display: 'none'}} /> <div className="ml-2"> <button className="p-2 mt-2 bg-gray-200 rounded-full" onClick={() => { const currentNames = getValues(`races.${index}.candidateNames`) || []; const updatedNames = currentNames.filter((_, idx) => idx !== i); setValue(`races.${index}.candidateNames`, updatedNames); setPCandidates(prev => ({ ...prev, [group.name]: prev[group.name] - 1 })); setFilledInputs(prev => { let obj = {...prev, [group.name]: prev[group.name] ? [...prev[group.name]] : []}; obj[group.name].splice(i, 1); return obj; }); }} > <Trash className="w-4 h-4" /> </button> </div> </div> ))} </div> </div> <button onClick={() => { setPCandidates((prev) => ({...prev, [group.name]: prev[group.name] + 1})); }} type="button" className={classNames( "w-auto font-semibold py-2 flex justify-start items-center -mt-6 mb-8 text-primary-blue rounded-md" )} > <PlusCircleIcon className="w-6 mr-2" /> Add Another Candidate </button> </> ) } }) } </div> </SlideOver> </div> </> ); }; export default ManageRaceItem;
问题原因分析
- React元素存储在状态中是反模式:原来的代码将生成的输入项元素存在
inputs状态里,每次pCandidates变化时,useEffect会重新生成所有元素并更新状态,容易导致元素重复挂载,事件绑定被多次注册。 - 事件处理中的副作用冲突:在onClick回调中同时调用
unregister和修改inputs状态,这两个操作都会触发组件重新渲染,导致事件被二次执行。 - 手动操作表单状态的风险:手动调用
unregister而不是通过React Hook Form的setValue更新数组,会导致表单状态和本地UI状态不一致,引发额外渲染。
修复说明
- 移除存储React元素的状态:直接在渲染阶段动态生成输入项,避免元素重复挂载和事件重复绑定。
- 使用React Hook Form的
setValue管理数组:通过setValue更新candidateNames数组,确保表单状态和UI同步,替代手动unregister操作。 - 同步更新所有相关状态:在移除操作中,同时更新表单状态、候选数量
pCandidates和已填充状态filledInputs,保证所有状态一致,减少不必要的渲染。
内容的提问来源于stack exchange,提问作者Dmitry
相关产品推荐
相关产品推荐

