如何在Formik+React DatePicker中显示MongoDB数据库日期
问题:Formik + React DatePicker无法显示MongoDB中的日期
我用Formik和自定义React DatePicker组件构建数据编辑表单,但没法把MongoDB里的日期显示在日期选择器输入框里。尝试过这些方法但都有问题:
- 在useEffect里用moment把日期格式化为
M/D/YYYY(和选择后的格式一致),但输入框是空的; - 改成
M-D-YYYY格式能显示,但点击输入框会触发时间格式错误; - 直接在Formik初始值里用moment对象,会触发“Maximum update depth exceeded”无限循环错误。
以下是相关代码:
CustomDatePicker组件
import { useField } from "formik"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; const CustomDatePicker = ({ label, name = "" }) => { const [field, meta, helpers] = useField(name); const { value } = meta; const { setValue } = helpers; return ( <> <label>{label}</label> <DatePicker {...field} selected={value} onChange={(date) => setValue(date)} /> </> ); }; export default CustomDatePicker;
调用代码
<Row> <Col className="d-flex flex-column my-2"> <CustomDatePicker label="Event End Date" name="eventEndDate" /> </Col> </Row>
完整EditReportModal表单代码
import { useEffect, useRef, useState } from "react"; import { Button, Modal, Row, Col } from "react-bootstrap"; import FormContainer from "./FormContainer"; import { Form, Formik } from "formik"; import { Tooltip } from "react-tooltip"; import Countries from "../countries-codes.json"; // icons import { BsFillQuestionDiamondFill } from "react-icons/bs"; import { useCreateReportMutation, useGetReportByIdQuery, } from "../slices/reportsApiSlice"; import { createReport } from "../schemas"; import moment from "moment"; import CustomRadio from "./inputs/CustomRadio"; import CustomSelect from "./inputs/CustomSelect"; import CustomInput from "./inputs/CustomInput"; import CustomDatePicker from "./inputs/CustomDatePicker"; import CustomTextarea from "./inputs/CustomTextarea"; import CustomCheckbox from "./inputs/CustomCheckbox"; import { toast } from "react-toastify"; import Loader from "./Loader"; const EditReportModal = ({ show, handleClose, editReportId, refetch }) => { const { data: myReport, isLoading } = useGetReportByIdQuery(editReportId); const [athlete, setAthlete] = useState(""); const [reportType, setReportType] = useState(""); const [matchType, setMatchType] = useState(""); const [eventName, setEventName] = useState(""); const [eventStartDate, setEventStartDate] = useState(""); useEffect(() => { if (myReport) { setReportType(myReport?.reportType); setMatchType(myReport?.matchType); setEventName(myReport?.eventName); setEventStartDate(moment(myReport?.eventStartDate).format("M/D/YYYY")); } }, [myReport]); const onSubmit = async (values, action) => {}; const windowSize = useRef([window.innerWidth, window.innerHeight]); const tooltipWidth = windowSize.current[0] > 500 ? "50vw" : "90vw"; return ( <Modal size="xl" show={show} onHide={handleClose}> <Modal.Header closeButton> <Modal.Title>Add Match</Modal.Title> </Modal.Header> <Modal.Body> {isLoading && <Loader />} <FormContainer> <Formik initialValues={{ athlete: "", reportType: "", matchType: "", eventName: "", eventStartDate: eventStartDate, eventEndDate: "", opponentName: "", opponentClub: "", opponentCountry: "", opponentRank: "", opponentHandedness: "", opponentAttacks: "", opponentAttackNotes: "", athleteAttachs: "", athleteAttackNotes: "", result: "", score: "", isPublic: "", }} validateSchema={createReport} onSubmit={onSubmit} > {({ isSubmitting }) => ( <Form> <Row> <Col className="d-flex flex-column my-2"> <label> Report Type <BsFillQuestionDiamondFill data-tooltip-id="report-type-tooltip" className="ms-2" /> <Tooltip id="report-type-tooltip" style={{ width: `${tooltipWidth}`, zIndex: "9999", }} > <div style={{ display: "flex", flexDirection: "column", }} > Select Result if you only want to record match results. <br /> Select Scouting if you are scouting an athlete for future matches <br /> Select both if you want to record results and scout the opponent for futuer matches. </div> </Tooltip> </label> <div className="d-flex flex-column align-items-start"> <CustomRadio label="Result" value="Result" name="reportType" placeholder="Result" checked={myReport?.reportType === "Result"} /> <CustomRadio label="Scouting" value="Scouting" name="reportType" checked={myReport?.reportType === "Scouting"} placeholder="Scouting" /> <CustomRadio label="Both" value="Both" name="reportType" placeholder="Both" checked={myReport?.reportType === "Both"} /> </div> </Col> </Row> <Row> <Col className="d-flex flex-column my-2"> <CustomSelect label="Match Type" name="matchType" placeholder="Select match type..." value={matchType} > <option value="">Select event type...</option> <option value="BJJ">Brazilian Jiu Jitsu</option> <option value="Judo">Judo</option> <option value="Wrestling">Wrestling</option> </CustomSelect> </Col> </Row> <Row> <Col className="d-flex flex-column my-2"> <CustomInput label="Event Name" name="eventName" type="text" placeholder="Enter event name" value={eventName} /> </Col> </Row> <Row> <Col className="d-flex flex-column my-2"> <CustomDatePicker label="Event Start Date" name="eventStartDate" /> </Col> <Col className="d-flex flex-column my-2"> <CustomDatePicker label="Event End Date" name="eventEndDate" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-2"> <CustomInput label="Opponent Name" name="opponentName" type="text" /> </Col> <Col className="d-flex flex-column my-2"> <CustomInput label="Opponent Club" name="opponentClub" type="text" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <CustomInput label="Opponent Rank" name="opponentRank" type="text" /> </Col> <Col className="d-flex flex-column my-3"> <label>Primarily right or left handed</label> <div className="d-flex flex-column align-items-start"> <CustomRadio label="Righty" value="righty" name="opponentHandedness" /> <CustomRadio label="Lefty" value="lefty" name="opponentHandedness" /> </div> </Col> </Row> <Row> <Col className="d-flex flex-column my-2"> <CustomSelect label="Country" name="opponentCountry" placeholder="Select country..." > <option value="">Select country...</option> <option value="USA">United States</option> <option value="">----------</option> {Countries.sort().map((country) => ( <option key={country.iso3_code} value={country.iso3_code} > {country.label_en} </option> ))} </CustomSelect> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <CustomTextarea label="Opponents attacks" type="textarea" name="opponentAttacks" placeholder="Enter opponent's attacks" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <CustomTextarea label="Notes on opponents attacks" type="textarea" name="opponentAttackNotes" placeholder="Enter notes on opponent's attacks" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <CustomTextarea label="My/My Athlete's attacks" type="textarea" name="athleteAttachs" placeholder="Enter My/My Athlete's attacks" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <CustomTextarea label="Notes on my/my athlete's attacks" type="textarea" name="athleteAttackNotes" placeholder="Enter notes on opponent's attacks" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <label>Outcome</label> <div className="d-flex flex-column align-items-start"> <CustomRadio label="Won" value="won" name="result" /> <CustomRadio label="Lost" value="Lost" name="result" /> </div> </Col> <Col className="d-flex flex-column my-3"> <CustomInput label="Score" name="score" type="text" /> </Col> </Row> <Row> <Col className="d-flex flex-column my-3"> <CustomCheckbox type="checkbox" text="Make this report public" name="isPublic" tooltipText="Make this report public" /> </Col> </Row> <Row> <Col className="d-flex flex-column align-items-center gap-2"> <Button disabled={isSubmitting} type="submit" className="login_btn" > Add Report </Button> </Col> </Row> </Form> )} </Formik> </FormContainer> </Modal.Body> </Modal> ); }; export default EditReportModal;
解决方案
核心问题分析
React DatePicker必须接收Date对象或moment对象作为selected属性的值,你之前把日期转成字符串格式,导致组件无法识别;直接用moment对象时,因为对象引用变化触发Formik无限重渲染。
步骤1:修复CustomDatePicker组件
让组件自动处理字符串到Date对象的转换,同时统一显示格式:
import { useField } from "formik"; import DatePicker from "react-datepicker"; import moment from "moment"; import "react-datepicker/dist/react-datepicker.css"; const CustomDatePicker = ({ label, name = "" }) => { const [field, meta, helpers] = useField(name); const { value } = meta; const { setValue } = helpers; // 解析字符串/日期为Date对象 const parseDate = (val) => { if (!val) return null; if (val instanceof Date || moment.isMoment(val)) return val; // 兼容多种日期格式 const parsed = moment(val, ["M/D/YYYY", "M-D-YYYY", "YYYY-MM-DD"], true); return parsed.isValid() ? parsed.toDate() : null; }; return ( <> <label>{label}</label> <DatePicker {...field} selected={parseDate(value)} onChange={(date) => setValue(date)} dateFormat="M/d/YYYY" // 统一显示格式 /> </> ); }; export default CustomDatePicker;
步骤2:优化EditReportModal的状态管理
去掉冗余本地状态,让Formik直接从接口数据生成初始值,避免状态不同步:
const EditReportModal = ({ show, handleClose, editReportId, refetch }) => { const { data: myReport, isLoading } = useGetReportByIdQuery(editReportId); // 直接从接口数据生成Formik初始值 const getInitialValues = () => { if (!myReport) { return { athlete: "", reportType: "", matchType: "", eventName: "", eventStartDate: null, eventEndDate: null, opponentName: "", opponentClub: "", opponentCountry: "", opponentRank: "", opponentHandedness: "", opponentAttacks: "", opponentAttackNotes: "", athleteAttachs: "", athleteAttackNotes: "", result: "", score: "", isPublic: "", }; } return { athlete: myReport.athlete || "", reportType: myReport.reportType || "", matchType: myReport.matchType || "", eventName: myReport.eventName || "", // 将MongoDB日期转为Date对象 eventStartDate: myReport.eventStartDate ? new Date(myReport.eventStartDate) : null, eventEndDate: myReport.eventEndDate ? new Date(myReport.eventEndDate) : null, opponentName: myReport.opponentName || "", opponentClub: myReport.opponentClub || "", opponentCountry: myReport.opponentCountry || "", opponentRank: myReport.opponentRank || "", opponentHandedness: myReport.opponentHandedness || "", opponentAttacks: myReport.opponentAttacks || "", opponentAttackNotes: myReport.opponentAttackNotes || "", athleteAttachs: myReport.athleteAttachs || "", athleteAttackNotes: myReport.athleteAttackNotes || "", result: myReport.result || "", score: myReport.score || "", isPublic: myReport.isPublic || "", }; }; const onSubmit = async (values, action) => { // 提交时将Date对象转为MongoDB兼容格式 const submitValues = { ...values, eventStartDate: values.eventStartDate ? moment(values.eventStartDate).format("YYYY-MM-DD") : null, eventEndDate: values.eventEndDate ? moment(values.eventEndDate).format("YYYY-MM-DD") : null, }; // 执行提交逻辑 action.setSubmitting(false); handleClose(); refetch(); }; const windowSize = useRef([window.innerWidth, window.innerHeight]); const tooltipWidth = windowSize.current[0] > 500 ? "50vw" : "90vw"; return ( <Modal size="xl" show={show} onHide={handleClose}> <Modal.Header closeButton> <Modal.Title>编辑赛事报告</Modal.Title> </Modal.Header> <Modal.Body> {isLoading && <Loader />} <FormContainer> <Formik initialValues={getInitialValues()} validateSchema={createReport} onSubmit={onSubmit} enableReinitialize // 关键:接口数据更新时自动重置表单 > {({ isSubmitting }) => ( <Form> <Row> <Col className="d-flex flex-column my-2"> <label> 报告类型 <BsFillQuestionDiamondFill data-tooltip-id="report-type-tooltip" className="ms-2" /> <Tooltip id="report-type-tooltip" style={{ width: `${tooltipWidth}`,
相关产品推荐
相关产品推荐

