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

