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

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状态不一致,引发额外渲染。

修复说明

  1. 移除存储React元素的状态:直接在渲染阶段动态生成输入项,避免元素重复挂载和事件重复绑定。
  2. 使用React Hook Form的setValue管理数组:通过setValue更新candidateNames数组,确保表单状态和UI同步,替代手动unregister操作。
  3. 同步更新所有相关状态:在移除操作中,同时更新表单状态、候选数量pCandidates和已填充状态filledInputs,保证所有状态一致,减少不必要的渲染。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 06:17:03