You need to enable JavaScript to run this app. 如何用React、Formik、Material UI实现分日/月/年下拉的出生日期输入组件
优惠活动
大模型
产品
解决方案
定价
更多

如何用React、Formik、Material UI实现分日/月/年下拉的出生日期输入组件

结合Formik实现三字段联动出生日期下拉组件

现有代码的核心问题

  • 完全没有接入Formik的状态管理体系,所有下拉框的选中值无法同步到Formik的全局表单数据中,提交时无法正常获取对应内容
  • 月、日、年三个字段相互独立没有联动,很容易出现选中2月30日这类无效日期的逻辑漏洞
  • 组件选型不匹配:Autocomplete是带搜索联想的可输入组件,适配长列表搜索场景,对于固定选项的出生日期选择来说,增加了不必要的交互复杂度

可直接落地的实现代码

import React, { useMemo } from 'react';
import { Grid, MenuItem, TextField } from '@material-ui/core';
import { useField, useFormikContext } from 'formik';

// 修正原代码里的月份拼写错误
const monthOptions = [
  { value: 1, label: 'January' },
  { value: 2, label: 'February' },
  { value: 3, label: 'March' },
  { value: 4, label: 'April' },
  { value: 5, label: 'May' },
  { value: 6, label: 'June' },
  { value: 7, label: 'July' },
  { value: 8, label: 'August' },
  { value: 9, label: 'September' },
  { value: 10, label: 'October' },
  { value: 11, label: 'November' },
  { value: 12, label: 'December' },
];

export default function DobSelector() {
  // 从Formik上下文获取全局表单值、字段更新方法、校验状态
  const { values, setFieldValue, errors, touched } = useFormikContext();
  // 分别绑定日、月、年三个独立表单字段,和原有编写逻辑对齐
  const [dayField] = useField('dob.day');
  const [monthField] = useField('dob.month');
  const [yearField] = useField('dob.year');

  // 生成年份选项,默认取当前年份往前80年的范围,可按需调整
  const yearOptions = useMemo(() => {
    const currentYear = new Date().getFullYear();
    return Array.from({ length: 80 }, (_, index) => currentYear - index);
  }, []);

  // 根据选中的年、月动态计算当月最大天数,自动兼容闰年、大小月规则
  const maxDay = useMemo(() => {
    if (!values.dob?.month) return 31;
    // 取当月下一个月的第0天,即为当月最后一天,是原生Date API的通用用法
    return new Date(
      values.dob.year || new Date().getFullYear(),
      values.dob.month,
      0
    ).getDate();
  }, [values.dob?.year, values.dob?.month]);

  // 生成对应天数的日选项
  const dayOptions = useMemo(() => {
    return Array.from({ length: maxDay }, (_, index) => index + 1);
  }, [maxDay]);

  // 自定义月份变更逻辑,兼容跨月时的日期值自动校正
  const handleMonthChange = (e) => {
    const selectedMonth = e.target.value;
    setFieldValue('dob.month', selectedMonth);
    // 计算新选中月份的最大天数
    const newMaxDay = new Date(
      values.dob.year || new Date().getFullYear(),
      selectedMonth,
      0
    ).getDate();
    // 如果之前选的日超过了新月份的最大天数,自动调整为当月最后一天
    if (values.dob?.day > newMaxDay) {
      setFieldValue('dob.day', newMaxDay);
    }
  };

  return (
    <Grid container spacing={2}>
      {/* 年份下拉框 */}
      <Grid item xs={4}>
        <TextField
          select
          label="Year"
          variant="outlined"
          fullWidth
          {...yearField}
          error={touched.dob?.year && Boolean(errors.dob?.year)}
          helperText={touched.dob?.year && errors.dob?.year}
        >
          <MenuItem value="" disabled>Select Year</MenuItem>
          {yearOptions.map(year => (
            <MenuItem key={year} value={year}>{year}</MenuItem>
          ))}
        </TextField>
      </Grid>
      {/* 月份下拉框 */}
      <Grid item xs={4}>
        <TextField
          select
          label="Month"
          variant="outlined"
          fullWidth
          {...monthField}
          onChange={handleMonthChange}
          error={touched.dob?.month && Boolean(errors.dob?.month)}
          helperText={touched.dob?.month && errors.dob?.month}
        >
          <MenuItem value="" disabled>Select Month</MenuItem>
          {monthOptions.map(month => (
            <MenuItem key={month.value} value={month.value}>{month.label}</MenuItem>
          ))}
        </TextField>
      </Grid>
      {/* 日下拉框 */}
      <Grid item xs={4}>
        <TextField
          select
          label="Day"
          variant="outlined"
          fullWidth
          {...dayField}
          disabled={!values.dob?.month}
          error={touched.dob?.day && Boolean(errors.dob?.day)}
          helperText={touched.dob?.day && errors.dob?.day}
        >
          <MenuItem value="" disabled>Select Day</MenuItem>
          {dayOptions.map(day => (
            <MenuItem key={day} value={day}>{day}</MenuItem>
          ))}
        </TextField>
      </Grid>
    </Grid>
  );
}

落地适配说明

  • 组件内部完全没有维护独立的本地状态,所有字段值的更新都通过Formik提供的方法同步,和你之前采用的单个下拉框开发思路完全统一,不需要适配新的写法
  • 日下拉框默认在未选中月份时为禁用状态,引导用户按年-月-日的顺序选择,减少无效操作
  • 年份的可选范围、日的选项规则都可以根据业务需求调整,比如注册场景可以过滤掉年龄小于13岁的年份选项,档案采集场景可以根据年龄段缩小年份范围
  • 校验逻辑不需要在组件内部实现,直接在Formik的全局校验规则里配置即可,比如要求三个字段都选中才能通过校验、判断最终拼接的日期是否合法

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.24 04:54:04