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
相关产品推荐
相关产品推荐

