如何在React TypeScript中集成自定义Dropdown与Formik并获取选中值
实现下拉选项值同步到Formik提交逻辑的方案
问题分析
现有代码中,FormDropdown已定义了和Formik交互的handleChange、handleBlur方法,但未传递给子组件Dropdown;同时Dropdown内部维护选中状态,但未对外暴露变更回调,也不支持接收外部值做受控同步,导致无法将选中值同步到Formik的表单状态中。
修改步骤
1. 改造Dropdown组件,支持受控属性与回调
让Dropdown接收外部传入的选中值、变更回调和失焦回调,并同步外部值到内部选中状态:
// Dropdown.tsx import React, { FC, Fragment, useEffect, useRef, useState } from "react"; import clsxm from "@/utils/clsxm"; import Button from "@/components/Button"; import type { DropdownProps, Option } from "./types"; import Typography from "../Typography"; const Dropdown: FC<DropdownProps> = ({ options, isScrolled = false, disabled = false, hasError = false, className, value, // 新增:接收外部选中值 onChange, // 新增:值变更时的回调 onBlur, // 新增:失焦回调 }) => { const [selectedOption, setSelectedOption] = useState<Option | null>(null); const [open, setIsOpen] = useState(false); const ref = useRef<any>(); // 同步外部传入的value到内部选中项 useEffect(() => { if (value) { const matchedOption = options.find(opt => opt.id === value); matchedOption && setSelectedOption(matchedOption); } else { setSelectedOption(null); } }, [value, options]); useEffect(() => { function handleClickOutside(event: any) { if (ref.current && !ref.current.contains(event.target)) { setIsOpen(false); onBlur?.(); // 点击外部时触发失焦 } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [ref, onBlur]); const handleClick = (option: Option) => () => { setSelectedOption(option); onChange?.(option.id); // 传递选中项的id给父组件 handleOpenClick(); }; const handleOpenClick = () => setIsOpen(prev => !prev); return ( <> <button type="button" className={clsxm( className, "w-full rounded-[0.938rem] border border-beluga bg-white px-4 py-4 text-base font-medium text-improbable focus-within:border-improbable", hasError ? "border-poppySurprise" : "" )} aria-expanded={open} aria-haspopup="true" onClick={handleOpenClick} onBlur={onBlur} > {selectedOption ? selectedOption.label : "Please Choose"} </button> {!disabled && open && ( <div ref={ref} className={clsxm( "absolute z-20 mt-[0.625rem] w-[14.0625rem] overflow-hidden rounded-[0.938rem] border-[0.0625rem] border-beluga bg-white py-[1.25rem]", isScrolled && "h-[15.5rem] overflow-scroll overflow-x-hidden" )} > {options.map(option => ( <Fragment key={option.id}> <Button className={clsxm("h-[0.75rem] w-[8.125rem] rounded-none border-none bg-transparent hover:bg-white")} onClick={handleClick(option)} > <Typography variant="p" size="text-sm" color="text-improbable"> {option.label} </Typography> </Button> </Fragment> ))} </div> )} </> ); }; export default Dropdown;
同步更新DropdownProps类型:
// Dropdown/types.ts export interface Option { id: string | number; label: string; } export interface DropdownProps { options: Option[]; isScrolled?: boolean; disabled?: boolean; hasError?: boolean; className?: string; value?: string | number; onChange?: (value: string | number) => void; onBlur?: () => void; }
2. 启用FormDropdown的属性传递
将FormDropdown中注释的属性传递给Dropdown,并适配回调参数:
// FormDropdown.tsx import { useField } from "formik"; import React, { FC, useCallback, useMemo, useState } from "react"; import { useUpdateEffect } from "@/hooks"; import Dropdown from "@/components/Dropdown"; import type { FormDropdownProps } from "../FormDropdown/types"; const FormDropdown: FC<FormDropdownProps> = (props) => { const { name } = props; const [, meta, helpers] = useField(name); const [currentValue, setCurrentValue] = useState<string | number>( meta.value || meta.initialValue ); const hasError = useMemo( () => meta.touched && !!meta.error, [meta.error, meta.touched] ); useUpdateEffect(() => setCurrentValue(meta.value), [meta.value]); const handleChange = useCallback( (value: string | number) => { setCurrentValue(value); helpers.setValue(value); helpers.setError(""); }, [helpers] ); const handleBlur = useCallback(() => helpers.setTouched(true), [helpers]); return ( <Dropdown {...props} value={currentValue} hasError={hasError} onChange={handleChange} onBlur={handleBlur} /> ); }; export default FormDropdown;
更新FormDropdownProps类型:
// FormDropdown/types.ts import type { DropdownProps } from "@/components/Dropdown/types"; export interface FormDropdownProps extends DropdownProps { name: string; }
3. 在addScreen.tsx中获取值用于提交
现在下拉选项变更时,值会自动同步到Formik的表单状态,直接在onSubmit回调中读取即可:
// addScreen.tsx import { Formik, Form } from "formik"; const AddScreen = () => { return ( <Formik initialValues={{ securityQuestion: "", // 其他表单字段... }} onSubmit={(values, { setSubmitting }) => { // 直接获取选中的安全问题ID console.log("选中的安全问题:", values.securityQuestion); // 执行提交逻辑,比如调用API setSubmitting(false); }} > {() => ( <Form> <FormDropdown options={[ { id: "what's your name?", label: "what's your name?" }, { id: "where do you live?", label: "where do you live?" }, { id: "what is your favorite food?", label: "what is your favorite food?" }, ]} name="securityQuestion" data-testid="securityQuestion" className="block h-full w-full border-transparent px-3 py-2 text-base leading-[1.813rem] text-nero focus:border-transparent focus:outline-none focus:ring-0" /> <button type="submit">提交</button> </Form> )} </Formik> ); }; export default AddScreen;
核心逻辑说明
Dropdown改为受控组件,通过外部value控制选中状态,onChange传递选中值FormDropdown通过Formik的useField钩子,将表单字段状态与Dropdown绑定,实现值的自动同步- 提交时直接从Formik的
values对象中读取securityQuestion字段即可使用
内容的提问来源于stack exchange,提问作者huda 23aduh
相关产品推荐
相关产品推荐

