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

使用HeadlessUI Combobox时Yup无法获取选中值的问题

问题:HeadlessUI Combobox结合Yup验证时始终触发必填错误

使用HeadlessUI的Combobox组件时,选中的值能正常写入selectedReward状态变量,但Yup仍持续抛出reward字段的必填错误提示。

相关代码

表单类型定义

// form values for Yup
type RewardCreateFormValues = {
    name: string
    price: number
    quantity: number
    reward: string
}

type Reward = {
    id: string
    name: string
    price: number
    quantity: number
}

const rewards: Reward[] = [
    {
        id: "1",
        name: "Reward 1",
        price: 100,
        quantity: 100
    },
    {
        id: "2",
        name: "Reward 2",
        price: 200,
        quantity: 200
    },
    {
        id: "3",
        name: "Reward 3",
        price: 300,
        quantity: 300
    }
]

Combobox状态与过滤逻辑

const [selectedReward, setSelectedReward] = useState(rewards[0])
const [query, setQuery] = useState("")

// it filters the rewards according to the query
let filteredReward =
    query === ""
        ? rewards
        : rewards.filter(reward => {
              return reward.name.toLowerCase().includes(query.toLowerCase())
          })

const rewardOptions = useMemo(() => {
    return filteredReward.map((reward: Reward) => ({
        name: reward.name,
        id: reward.id
    }))
}, [filteredReward])

Yup验证规则与初始值

const validationSchema = Yup.object().shape({
    name: Yup.string().required("Required"),
    price: Yup.number().required("Required"),
    quantity: Yup.number().required("Required"),
    reward: Yup.string().required("Required")
})

const initialValues: RewardCreateFormValues = {
    name: "",
    price: 0,
    quantity: 0,
    reward: ""
}

Combobox组件代码

<GridItem colSpan={3}>
    <FormControl>
        <Box>
            <FormLabel>Select a Reward</FormLabel>
            <Combobox as="div" value={selectedReward} onChange={setSelectedReward}>
                <Combobox.Button as="div">
                    <Combobox.Input
                        className="w-full rounded-md border-0 bg-white py-2 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-[#E2E8F0] focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                        name="reward"
                        onChange={(event: any) => setQuery(event.target.value)}
                        placeholder="Select a Reward"
                        displayValue={(selectedReward: Reward) =>
                            selectedReward ? selectedReward.name : ""
                        }
                    />
                </Combobox.Button>
                <Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
                    {rewardOptions.map(reward => (
                        <Combobox.Option
                            key={reward.id}
                            value={reward}
                            className={({ active }) =>
                                classNames(
                                    "relative cursor-default select-none py-2 pl-3 pr-9",
                                    active ? "bg-indigo-600 text-white" : "text-gray-900"
                                )
                            }
                        >
                            {({ active, selected }) => (
                                <>
                                    <Box display="flex">
                                        <Text
                                            as="span"
                                            className={classNames(
                                                "truncate",
                                                selected ? "font-semibold" : "font-normal"
                                            )}
                                        >
                                            {reward.name}
                                        </Text>
                                    </Box>

                                    {selected && (
                                        <Text
                                            as="span"
                                            className={classNames(
                                                "absolute inset-y-0 right-0 flex items-center pr-4",
                                                active ? "text-white" : "text-indigo-600"
                                            )}
                                        >
                                            <CheckIcon
                                                className="h-5 w-5"
                                                aria-hidden="true"
                                            />
                                        </Text>
                                    )}
                                </>
                            )}
                        </Combobox.Option>
                    ))}
                    <Combobox.Option
                        value=""
                        className={
                            "relative cursor-default select-none py-2 pl-3 pr-9 bg-indigo-100 hover:bg-indigo-600 hover:text-white text-gray-900"
                        }
                        onClick={onOpen}
                    >
                        <Box
                            display="flex"
                            flexDirection="row"
                            gap={2}
                            alignItems="center"
                        >
                            <PlusIcon className="h-5 w-5" aria-hidden="true" />
                            <button>Create a Reward</button>
                        </Box>
                    </Combobox.Option>
                </Combobox.Options>
            </Combobox>
        </Box>
        <ErrorMessage name="reward" component="p" className="mt-2 text-sm text-red-600" />
    </FormControl>
</GridItem>
<GridItem colSpan={3}>
    <Box display="flex" justifyContent="flex-end">
        <Button
            isLoading={isSubmitting}
            isDisabled={isSubmitting}
            type="submit"
            colorScheme="indigo"
            variant="solid"
        >
            Create Listing
        </Button>
    </Box>
</GridItem>

解决方案

问题核心是表单库(如Formik)的表单状态与自定义的selectedReward状态未同步,同时存在类型不匹配问题,导致Yup验证的reward字段始终为空。

1. 同步表单字段与Combobox选中值

在Combobox的onChange事件中,不仅更新selectedReward,还要同步更新表单的reward字段(以Formik为例,使用setFieldValue):

<Combobox 
  as="div" 
  value={selectedReward} 
  onChange={(selectedItem) => {
    setSelectedReward(selectedItem);
    // 同步到表单的reward字段,传入选中项的id
    setFieldValue('reward', selectedItem.id);
  }}
>

2. 修正初始值不匹配

selectedReward初始值是rewards[0],但表单初始值的reward为空字符串,导致初始状态验证不通过。修改初始值为对应选中项的id:

const initialValues: RewardCreateFormValues = {
    name: "",
    price: 0,
    quantity: 0,
    reward: rewards[0].id // 改为初始选中reward的id
}

3. 统一Combobox选项的类型

当前rewardOptions返回仅包含name和id的对象,而selectedReward是完整的Reward对象,类型不匹配会导致选中逻辑异常。调整rewardOptions直接返回完整的Reward对象:

const rewardOptions = useMemo(() => {
    return filteredReward; // 直接返回完整的Reward数组
}, [filteredReward])

同时修改Combobox.Option的value为完整对象:

{rewardOptions.map(reward => (
    <Combobox.Option
        key={reward.id}
        value={reward} // 传入完整的Reward对象
        className={({ active }) =>
            classNames(
                "relative cursor-default select-none py-2 pl-3 pr-9",
                active ? "bg-indigo-600 text-white" : "text-gray-900"
            )
        }
    >
        {/* 原有内容不变 */}
    </Combobox.Option>
))}

4. 处理"Create a Reward"选项

该选项的value为空字符串,选中后会导致reward字段为空,需确保点击该按钮时不更新表单字段,而是直接打开创建弹窗:

<Combobox.Option
    value={null} // 改为null,避免空字符串干扰验证
    className={
        "relative cursor-default select-none py-2 pl-3 pr-9 bg-indigo-100 hover:bg-indigo-600 hover:text-white text-gray-900"
    }
    onClick={(e) => {
        e.preventDefault(); // 阻止Combobox的选中逻辑
        onOpen(); // 打开创建弹窗
    }}
>
    {/* 原有内容不变 */}
</Combobox.Option>

完成以上修改后,表单的reward字段会与Combobox选中值同步,Yup验证就能正常工作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 15:12:11