useMuiXMonthCalendarAdapter
Returns adapter props for MUI X MonthCalendar with React Hook Form, and explains why month rules belong in shouldDisableMonth rather than disablePast or minDate.
useMuiXMonthCalendarAdapter returns React Hook Form props for MUI X's MonthCalendar, a grid of the twelve months of one year. It's what MuiXMonthCalendarElement uses internally.
Use the hook instead of the element when you need to show the validation message, add a visible label for the grid, or keep month rules for something like a billing start in one component.
Import
import { useMuiXMonthCalendarAdapter } from "@piplup/rhf-adapters/mui-x-date-pickers";Usage
This BillingMonthField accepts the current month or a later one. Submit without choosing to see the required message under the grid.
"use client";import dayjs, { type Dayjs } from "dayjs";import FormHelperText from "@mui/material/FormHelperText";import Stack from "@mui/material/Stack";import Typography from "@mui/material/Typography";import { LocalizationProvider, MonthCalendar } from "@mui/x-date-pickers";import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";import { MuiButtonElement } from "@piplup/rhf-adapters/mui-material";import { useMuiXMonthCalendarAdapter } from "@piplup/rhf-adapters/mui-x-date-pickers";import { type Control, useForm } from "react-hook-form";type FormValues = { billingStart: Dayjs | null };// Compare by month, not by day. MonthCalendar stores the 1st of the month when// nothing is selected yet, so `disablePast` would reject the current month.function isPastMonth(month: Dayjs) { return month.isBefore(dayjs(), "month");}function BillingMonthField({ control }: { control: Control<FormValues> }) { // Remove the props MonthCalendar does not accept; it would pass them to its root <div>. const { error, helperText, required: _required, shouldDisableDate: _shouldDisableDate, shouldDisableYear: _shouldDisableYear, ...adapter } = useMuiXMonthCalendarAdapter< Dayjs, FormValues, "billingStart", HTMLDivElement >({ control, name: "billingStart", required: true, composeHelperText: true, shouldDisableMonth: isPastMonth, messages: { required: "Choose the first month to bill", shouldDisableMonth: "Billing can't start in a past month", }, }); return ( <div> <Typography variant="subtitle2" id="billing-start-label"> First billing month ({dayjs().year()}) </Typography> <MonthCalendar gridLabelId="billing-start-label" {...adapter} /> <FormHelperText error={error}>{helperText}</FormHelperText> </div> );}export default function Page() { const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { billingStart: null }, }); return ( <LocalizationProvider dateAdapter={AdapterDayjs}> <form noValidate onSubmit={handleSubmit((values) => alert(values.billingStart?.startOf("month").format("YYYY-MM-DD")), )} > <Stack direction="column" spacing={2}> <BillingMonthField control={control} /> <div> <MuiButtonElement control={control} variant="contained" type="submit"> Start subscription </MuiButtonElement> </div> </Stack> </form> </LocalizationProvider> );}How it works
The wiring is the same as useMuiXDateCalendarAdapter: there is no text field, so render the message yourself. MonthCalendar passes unknown props to its root <div>, and the result contains more of them than the DateCalendar one: remove error, helperText, required, shouldDisableDate and shouldDisableYear before spreading, as the example does. Otherwise React warns about unknown DOM props. The returned onBlur also lands on that root, so the field becomes touched when focus leaves a month button.
The value is still a full date. Selecting a month calls the date library's "set month" on the current value, so the day and time are kept. If the field is empty, MonthCalendar starts from the first day of the current month. In the example, picking March stores 1 March of this year. Normalize it on submit (the example calls startOf("month")) or with transform.output.
Validation compares days, not months. The hook runs the same date validation as useMuiXDatePickerAdapter, with its keys and default messages. disablePast, disableFuture, minDate and maxDate are checked to the day, while MonthCalendar enables and disables whole months.
With disablePast, picking the current month on an empty field stores the 1st of the month. Unless today is the 1st, that date is in the past and the form rejects a month the grid shows as enabled. A mid-month minDate has the same problem. Express month limits with shouldDisableMonth instead, comparing by month as the example does: (month) => month.isBefore(dayjs(), "month").
shouldDisableMonth is checked in both places: MonthCalendar uses it to disable buttons, and the hook uses it to validate the stored value with the shouldDisableMonth message. shouldDisableDate and shouldDisableYear are accepted and validated too, but MonthCalendar does not use them to disable buttons, which is why they are removed before spreading.
Props
Prop
Type
Return value
Prop
Type
useMuiXMobileTimePickerAdapter
Returns adapter props for building custom MUI X MobileTimePicker integrations with React Hook Form, where the time is chosen on an analog clock in a dialog.
useMuiXMultiSectionDigitalClockAdapter
Returns adapter props for building custom MUI X MultiSectionDigitalClock integrations with React Hook Form, with per-section (hours, minutes, seconds) validation messages.