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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 23:21:06