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

React模态框重复选项禁用问题:移除功能bug修复求助

React模态框移除选项后放回可用列表的Bug修复

我开发了一个用于收集表单数据并过滤列表的React模态框,包含多个选择输入框,需求是禁止用户选择重复选项。点击「添加」图标时,选中的选项会从选择框的可用选项中移除,这个功能正常;但点击「移除」图标时,需要将之前选中的选项重新放回可用选项,目前这个功能存在Bug。

原代码

父组件代码

const filterOptions = [
  { id: 1, value: "group", label: "group" },
  { id: 2, value: "id", label: "id" },
  { id: 3, value: "title", label:"title" },
  { id: 4, value: "owners", label: "owners" },
  { id: 5, value: "users", label: "users" },
  { id: 6, value: "responsibles", label: "responsibles" },
  { id: 7, value: "reviewDate", label: "reviewDate" },
  { id: 8, value: "description", label:"description" },
];

const equalOptions = [
  { value: "=", label: "equal" },
  { value: "!=", label: "not-equal" },
  { value: "LIKE", label: "like" },
];

const groupOptions = [
  { value: "1", label: " user1" },
  { value: "2", label: "user 2" },
];

const PropertyFilterListModal: React.FC<Props> = ({ onClose }) => {
  const generateId = () =>
    Math.floor(new Date().getTime() * Math.random() * 1000);

  const defaultItem = () => ({
    id: generateId(),
    title: "",
    impact: "",
  });

  const { control, getValues } = useForm<FieldValues>();


  const [propertyFilters, setPropertyFilters] = useState([defaultItem()]);
  const [updatedOptions, setUpdatedOptions] = useState<FilterProps[]>(filterOptions);
  const [selectedVal, setSelectedVal] = useState("");

  const handleAdd = () => {
    setPropertyFilters((prev) => [...prev, defaultItem()]);
    setUpdatedOptions((prev) =>
      prev.filter((option) => option.value !== selectedVal)
    );
  };

  const handleRemove = (item: any) => {
    const updatePropertyFilters = [...propertyFilters];
    const colVal = getValues(`${item.id?.toString()}.filterOptions` as any);
    const removedOption = filterOptions.find(
      (option) => option.value === colVal
    );
    setUpdatedOptions((prev) => [removedOption, ...prev]);
    const ind = updatePropertyFilters.findIndex((q) => q.id === item.id);
    if (ind !== -1) {
      updatePropertyFilters.splice(ind, 1);
      setPropertyFilters(updatePropertyFilters);
    }
  };

  const handleUsedFilter = (val: any) => {
    setSelectedVal(val);
  };


  return (
    <Modal isOpen onClose={onClose} >
      <form className="w-[38rem] h-[24rem]" onSubmit={onSubmit}>
        <section>
          {propertyFilters.map((item, ind) => (
            <section className="flex items-center justify-between">
              <PropertyFilterItem
                key={item.id}
                index={ind + 1}
                control={control}
                item={item}
                filterOptions={updatedOptions}
                equalOptions={equalOptions}
                groupOptions={groupOptions}
                onFilterChanged={handleUsedFilter}
              />

              {propertyFilters.length === 1 ? null : (
                <div className="mb-3 w-9">
                  <span className="w-1/12 block ">
                    <span
                      className="block cursor-pointer transition text-red-500 hover:text-red-800"
                      onClick={() => handleRemove(item)}
                    >
                      <Remove />
                    </span>
                  </span>
                </div>
              )}
            </section>
          ))}

          <div>
            <span className="w-1/12 ml-2 flex items-center justify-center">
              <span
                className="block cursor-pointer transition text-green-500 hover:text-green-800"
                onClick={handleAdd}
              >
                <Add />
              </span>
            </span>
          </div>
        </section>
        <section className="flex justify-end mt-5 gap-3">
          <Button size="lg" color="danger">
            cancle
          </Button>
          <Button size="lg" color="success">
            view
          </Button>
        </section>
      </form>
    </Modal>
  );
};

export default PropertyFilterListModal;

子组件代码

const PropertyFilterItem: React.FC<Props> = ({
  control,
  item,
  filterOptions,
  equalOptions,
  groupOptions,
  onFilterChanged,
}) => {
  const [showInput, setShowInput] = useState(true);
  const [showSelect, setShowSelect] = useState(false);

  const handleChangeEqualOptions = (value: string | number) => {
    console.log(value);
    switch (value) {
      case "LIKE":
        setShowInput(true);
        setShowSelect(false);
        break;
    }
  };

  const handleChangeFilterOptions = (value: string | number) => {
    onFilterChanged(value);
    switch (value) {
      case "owners":
      case "responsibles":
      case "users":
        setShowInput(false);
        setShowSelect(true);
        break;
      default:
        setShowInput(true);
        setShowSelect(false);
        break;
    }
  };

  return (
    <div className="flex w-full items-center gap-2">
      <SelectController
        formClassName="w-[176px]"
        control={control}
        options={filterOptions}
        name={`${item.id?.toString()}.filterOptions`}
        onChangeValue={handleChangeFilterOptions}
      />
      <SelectController
        formClassName="w-[128px]"
        name={`${item.id?.toString()}.equalOptions`}
        control={control}
        options={equalOptions}
        onChangeValue={handleChangeEqualOptions}
      />
      {showInput && (
        <InputController
          name={`${item.id?.toString()}.input`}
          control={control}
          formClassName="w-[248px]"
        />
      )}
      {showSelect && (
        <SelectController
          formClassName="w-[248px]"
          name={`${item.id?.toString()}.select`}
          control={control}
          options={groupOptions}
          isMulti
        />
      )}
    </div>
  );
};

export default PropertyFilterItem;

Bug分析

当前实现的核心问题在于依赖selectedVal和updatedOptions手动维护状态,容易导致状态不同步:

  • 用户修改已存在筛选项的选择时,selectedVal会被更新,后续添加新项时会错误移除最新修改的选项,而非新添加项选中的选项
  • handleRemove中直接将移除的选项添加到updatedOptions,但未考虑其他已选中的选项,可能导致重复添加
  • 使用getValues获取移除项的选中值时,若表单值未及时同步,可能拿到错误数据

修复方案

放弃手动维护状态,改为实时计算可用选项:基于所有已选中的筛选项,从原始filterOptions中过滤掉已选值,确保状态始终同步,无需手动处理添加/移除时的选项列表。

修改后的代码

父组件代码

const filterOptions = [
  { id: 1, value: "group", label: "group" },
  { id: 2, value: "id", label: "id" },
  { id: 3, value: "title", label:"title" },
  { id: 4, value: "owners", label: "owners" },
  { id: 5, value: "users", label: "users" },
  { id: 6, value: "responsibles", label: "responsibles" },
  { id: 7, value: "reviewDate", label: "reviewDate" },
  { id: 8, value: "description", label:"description" },
];

const equalOptions = [
  { value: "=", label: "equal" },
  { value: "!=", label: "not-equal" },
  { value: "LIKE", label: "like" },
];

const groupOptions = [
  { value: "1", label: " user1" },
  { value: "2", label: "user 2" },
];

const PropertyFilterListModal: React.FC<Props> = ({ onClose }) => {
  const generateId = () =>
    Math.floor(new Date().getTime() * Math.random() * 1000);

  const defaultItem = () => ({
    id: generateId(),
    title: "",
    impact: "",
  });

  const { control, getValues } = useForm<FieldValues>();

  const [propertyFilters, setPropertyFilters] = useState([defaultItem()]);

  // 实时计算可用选项:过滤掉所有已选中的filterOptions值
  const availableFilterOptions = useMemo(() => {
    const allSelectedFilters = propertyFilters.map(item => 
      getValues(`${item.id.toString()}.filterOptions`) as string
    ).filter(Boolean); // 过滤未选中的值
    
    return filterOptions.filter(option => 
      !allSelectedFilters.includes(option.value)
    );
  }, [propertyFilters, getValues]);

  const handleAdd = () => {
    setPropertyFilters((prev) => [...prev, defaultItem()]);
  };

  const handleRemove = (item: any) => {
    setPropertyFilters(prev => prev.filter(q => q.id !== item.id));
  };

  return (
    <Modal isOpen onClose={onClose} >
      <form className="w-[38rem] h-[24rem]" onSubmit={onSubmit}>
        <section>
          {propertyFilters.map((item, ind) => (
            <section className="flex items-center justify-between" key={item.id}>
              <PropertyFilterItem
                index={ind + 1}
                control={control}
                item={item}
                filterOptions={availableFilterOptions}
                equalOptions={equalOptions}
                groupOptions={groupOptions}
              />

              {propertyFilters.length === 1 ? null : (
                <div className="mb-3 w-9">
                  <span className="w-1/12 block ">
                    <span
                      className="block cursor-pointer transition text-red-500 hover:text-red-800"
                      onClick={() => handleRemove(item)}
                    >
                      <Remove />
                    </span>
                  </span>
                </div>
              )}
            </section>
          ))}

          <div>
            <span className="w-1/12 ml-2 flex items-center justify-center">
              <span
                className="block cursor-pointer transition text-green-500 hover:text-green-800"
                onClick={handleAdd}
              >
                <Add />
              </span>
            </span>
          </div>
        </section>
        <section className="flex justify-end mt-5 gap-3">
          <Button size="lg" color="danger">
            cancel
          </Button>
          <Button size="lg" color="success">
            view
          </Button>
        </section>
      </form>
    </Modal>
  );
};

export default PropertyFilterListModal;

子组件代码

const PropertyFilterItem: React.FC<Props> = ({
  control,
  item,
  filterOptions,
  equalOptions,
  groupOptions,
}) => {
  const [showInput, setShowInput] = useState(true);
  const [showSelect, setShowSelect] = useState(false);

  const handleChangeEqualOptions = (value: string | number) => {
    switch (value) {
      case "LIKE":
        setShowInput(true);
        setShowSelect(false);
        break;
      default:
        break;
    }
  };

  const handleChangeFilterOptions = (value: string | number) => {
    switch (value) {
      case "owners":
      case "responsibles":
      case "users":
        setShowInput(false);
        setShowSelect(true);
        break;
      default:
        setShowInput(true);
        setShowSelect(false);
        break;
    }
  };

  return (
    <div className="flex w-full items-center gap-2">
      <SelectController
        formClassName="w-[176px]"
        control={control}
        options={filterOptions}
        name={`${item.id?.toString()}.filterOptions`}
        onChangeValue={handleChangeFilterOptions}
      />
      <SelectController
        formClassName="w-[128px]"
        name={`${item.id?.toString()}.equalOptions`}
        control={control}
        options={equalOptions}
        onChangeValue={handleChangeEqualOptions}
      />
      {showInput && (
        <InputController
          name={`${item.id?.toString()}.input`}
          control={control}
          formClassName="w-[248px]"
        />
      )}
      {showSelect && (
        <SelectController
          formClassName="w-[248px]"
          name={`${item.id?.toString()}.select`}
          control={control}
          options={groupOptions}
          isMulti
        />
      )}
    </div>
  );
};

export default PropertyFilterItem;

修复后优势

  • 所有可用选项实时计算,彻底避免状态不同步问题
  • 移除项后,可用选项自动重新包含该选项,无需手动添加
  • 用户修改已存在筛选项时,可用选项自动更新,确保无重复选择
  • 代码更简洁,逻辑更清晰,减少状态维护复杂度

内容的提问来源于stack exchange,提问作者Mehrdad Karami Jourabi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 15:57:26