Menu

How to Use React MUI Date and Time Pickers to Build a Custom Booking Form

Building a booking form is a common requirement in many web applications, from appointment schedulers to event registration systems. Material UI (MUI) offers powerful date and time picker components that can simplify this process significantly. In this article, I'll walk you through creating a comprehensive booking form using MUI's date and time pickers.

What You'll Learn

By the end of this tutorial, you'll be able to:

  • Implement MUI date and time pickers in a React application
  • Create a fully functional booking form with validation
  • Customize the appearance and behavior of the pickers
  • Handle form state and submission
  • Implement advanced features like date range selection and time constraints
  • Troubleshoot common issues that arise when working with date/time components

Understanding MUI Date and Time Pickers

Before diving into implementation, let's understand what MUI offers for date and time selection. MUI's date and time pickers are part of the MUI X package, which provides advanced components beyond the core MUI library. These components offer a rich set of features for selecting dates and times with a polished UI that follows Material Design principles.

The date and time pickers in MUI X come in several variants, including basic date pickers, time pickers, date-time pickers, and date range pickers. They support both mobile and desktop interfaces, various formatting options, and can be easily integrated with form libraries like Formik or React Hook Form.

Key Components Overview

MUI X provides several picker components that we'll use in our booking form:

  1. DatePicker - For selecting a single date
  2. TimePicker - For selecting a time
  3. DateTimePicker - For selecting both date and time together
  4. DateRangePicker - For selecting a range of dates
  5. StaticDatePicker - For embedding a calendar directly in the UI

Each of these components is highly customizable and can be configured to meet specific requirements.

Setting Up Your Project

Let's start by setting up a React project with MUI and the necessary date picker components.

Installation

First, we need to create a React application and install the required dependencies:

# Create a new React project
npx create-react-app mui-booking-form

# Navigate to the project directory
cd mui-booking-form

# Install MUI core
npm install @mui/material @emotion/react @emotion/styled

# Install MUI X Date Pickers and date library
npm install @mui/x-date-pickers dayjs

I'm using dayjs as the date library here, but MUI X Date Pickers also supports other libraries like moment, luxon, or date-fns. Choose the one you're most comfortable with.

Configuring Date Providers

Before we can use the date picker components, we need to set up the date provider. This provider makes the date library available to all the picker components in your application.

Let's modify our App.js file to include the necessary providers:

import React from 'react';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import BookingForm from './BookingForm';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';

// Create a theme instance
const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2',
    },
    secondary: {
      main: '#dc004e',
    },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <LocalizationProvider dateAdapter={AdapterDayjs}>
        <div className="App">
          <header className="App-header">
            <h1>Booking System</h1>
          </header>
          <main>
            <BookingForm />
          </main>
        </div>
      </LocalizationProvider>
    </ThemeProvider>
  );
}

export default App;

In this setup, we've:

  1. Wrapped our application with ThemeProvider to apply consistent styling
  2. Added LocalizationProvider with the DayJS adapter
  3. Included a placeholder for our BookingForm component that we'll create next

Deep Dive into MUI Date and Time Picker Components

Before building our booking form, let's explore the MUI Date and Time Picker components in detail to understand what they offer.

DatePicker Component

The DatePicker component allows users to select a date from a calendar interface. Here's a comprehensive look at its capabilities:

Essential Props

PropTypeDefaultDescription
valueDate | nullnullThe selected date value
onChangefunctionrequiredCallback fired when the value changes
disabledbooleanfalseIf true, the picker and text field are disabled
disableFuturebooleanfalseIf true, disables future dates
disablePastbooleanfalseIf true, disables past dates
minDateDate-The minimum selectable date
maxDateDate-The maximum selectable date
shouldDisableDatefunction() => falseCustom function to disable specific dates
viewsarray['year', 'day']Available views (year, month, day)
openTostring'day'The view to open with
formatstring'MM/DD/YYYY'Format string for the date

Controlled vs Uncontrolled Usage

The DatePicker can be used in either controlled or uncontrolled mode:

Controlled mode (recommended):

import { useState } from 'react';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';

function ControlledDatePicker() {
  const [value, setValue] = useState(dayjs());
  
  return (
    <DatePicker
      label="Controlled date"
      value={value}
      onChange={(newValue) => setValue(newValue)}
    />
  );
}

Uncontrolled mode:

import { DatePicker } from '@mui/x-date-pickers/DatePicker';

function UncontrolledDatePicker() {
  return (
    <DatePicker
      label="Uncontrolled date"
      defaultValue={dayjs()}
    />
  );
}

Customization Options

The DatePicker can be customized in several ways:

  1. Styling with the sx prop:
<DatePicker
  label="Custom styled date picker"
  sx={{
    '& .MuiInputBase-root': {
      borderRadius: 2,
    },
    '& .MuiOutlinedInput-notchedOutline': {
      borderColor: 'primary.main',
    },
  }}
/>
  1. Theme customization:
const theme = createTheme({
  components: {
    MuiDatePicker: {
      styleOverrides: {
        root: {
          backgroundColor: '#f5f5f5',
        },
      },
    },
  },
});
  1. Custom day rendering:
<DatePicker
  label="Custom day rendering"
  slots={{
    day: (props) => {
      const isSelected = 
        !props.outsideCurrentMonth && 
        props.day.toString() === props.selectedDay?.toString();
      
      return (
        <div
          {...props}
          style={{
            ...props.style,
            backgroundColor: isSelected ? 'lightblue' : 'transparent',
            borderRadius: '50%',
          }}
        >
          {props.day.getDate()}
        </div>
      );
    },
  }}
/>

Accessibility Features

The DatePicker component comes with built-in accessibility features:

  • Keyboard navigation (arrow keys to navigate dates)
  • Screen reader support
  • ARIA attributes
  • Focus management

To enhance accessibility further, you can add:

<DatePicker
  label="Appointment Date"
  aria-label="Choose your appointment date"
  slotProps={{
    textField: {
      helperText: 'MM/DD/YYYY format',
      InputProps: {
        'aria-describedby': 'date-picker-helper-text',
      },
    },
  }}
/>

TimePicker Component

The TimePicker component allows users to select a time. Here's a detailed look:

Essential Props

PropTypeDefaultDescription
valueDate | nullnullThe selected time value
onChangefunctionrequiredCallback fired when the value changes
ampmbooleantrue (US locale)12h/24h view for hour selection clock
minutesStepnumber1Step over minutes
minTimeDate-Minimum allowed time
maxTimeDate-Maximum allowed time
shouldDisableTimefunction() => falseCustom function to disable specific times
viewsarray['hours', 'minutes']Available views
formatstring'hh:mm a'Format string for the time

Usage Example

import { useState } from 'react';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';

function TimePickerExample() {
  const [value, setValue] = useState(dayjs().hour(9).minute(0));
  
  return (
    <TimePicker
      label="Appointment Time"
      value={value}
      onChange={(newValue) => setValue(newValue)}
      ampm={true}
      minutesStep={15}
    />
  );
}

Customizing Time Constraints

You can limit available times using various props:

// Only allow business hours (9 AM - 5 PM)
<TimePicker
  label="Business Hours Only"
  value={value}
  onChange={handleChange}
  minTime={dayjs().hour(9).minute(0)}
  maxTime={dayjs().hour(17).minute(0)}
/>

Or disable specific times:

// Disable lunch hour (12-1 PM)
<TimePicker
  label="Excluding Lunch Hour"
  value={value}
  onChange={handleChange}
  shouldDisableTime={(timeValue, view) => {
    if (view === 'hours' && timeValue.hour() === 12) {
      return true;
    }
    return false;
  }}
/>

DateTimePicker Component

The DateTimePicker combines both date and time selection in one component:

import { useState } from 'react';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import dayjs from 'dayjs';

function DateTimePickerExample() {
  const [value, setValue] = useState(dayjs());
  
  return (
    <DateTimePicker
      label="Appointment Date & Time"
      value={value}
      onChange={(newValue) => setValue(newValue)}
      disablePast
      minutesStep={15}
    />
  );
}

This component inherits props from both DatePicker and TimePicker, giving you comprehensive control over both date and time selection.

DateRangePicker Component

For selecting a range of dates, MUI X provides the DateRangePicker:

import { useState } from 'react';
import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
import dayjs from 'dayjs';

function DateRangePickerExample() {
  const [value, setValue] = useState([dayjs(), dayjs().add(7, 'day')]);
  
  return (
    <DateRangePicker
      label="Date Range"
      value={value}
      onChange={(newValue) => setValue(newValue)}
      disablePast
    />
  );
}

Note: The DateRangePicker is part of the Pro package of MUI X, which requires a license for commercial use.

Building the Booking Form Step by Step

Now that we understand the components, let's build our booking form. We'll create a form that allows users to book appointments with date, time, and additional details.

Step 1: Create the Basic Form Structure

First, let's create a new file called BookingForm.js with the basic structure:

import React, { useState } from 'react';
import {
  Box,
  Paper,
  Typography,
  TextField,
  Button,
  Grid,
  MenuItem,
  Snackbar,
  Alert,
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';

const BookingForm = () => {
  // Form state
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: '',
    service: '',
    date: dayjs(),
    time: dayjs().hour(9).minute(0),
    notes: '',
  });
  
  // Form submission state
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitSuccess, setSubmitSuccess] = useState(false);
  const [submitError, setSubmitError] = useState(false);
  
  // Handle input changes
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };
  
  // Handle date change
  const handleDateChange = (newDate) => {
    setFormData({
      ...formData,
      date: newDate,
    });
  };
  
  // Handle time change
  const handleTimeChange = (newTime) => {
    setFormData({
      ...formData,
      time: newTime,
    });
  };
  
  // Handle form submission
  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    // Simulate API call
    setTimeout(() => {
      console.log('Form submitted:', formData);
      setIsSubmitting(false);
      setSubmitSuccess(true);
      // Reset form after successful submission
      setFormData({
        name: '',
        email: '',
        phone: '',
        service: '',
        date: dayjs(),
        time: dayjs().hour(9).minute(0),
        notes: '',
      });
    }, 1500);
  };
  
  // Available services
  const services = [
    'Consultation',
    'Haircut',
    'Massage',
    'Dental Checkup',
    'Fitness Training',
  ];
  
  return (
    <Box sx={{ maxWidth: 800, mx: 'auto', p: 2 }}>
      <Paper elevation={3} sx={{ p: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          Book Your Appointment
        </Typography>
        <Typography variant="body1" sx={{ mb: 4 }}>
          Fill out the form below to schedule your appointment.
        </Typography>
        
        <form onSubmit={handleSubmit}>
          <Grid container spacing={3}>
            {/* Form fields will go here */}
          </Grid>
        </form>
      </Paper>
      
      {/* Success/Error messages */}
      <Snackbar 
        open={submitSuccess} 
        autoHideDuration={6000} 
        onClose={() => setSubmitSuccess(false)}
      >
        <Alert severity="success">Appointment booked successfully!</Alert>
      </Snackbar>
      
      <Snackbar 
        open={submitError} 
        autoHideDuration={6000} 
        onClose={() => setSubmitError(false)}
      >
        <Alert severity="error">There was an error booking your appointment. Please try again.</Alert>
      </Snackbar>
    </Box>
  );
};

export default BookingForm;

This sets up our basic form structure with state management for the form fields and submission status. We've also included a simulated API call to demonstrate how you would handle form submission.

Step 2: Add Personal Information Fields

Let's add the personal information fields to our form:

{/* Personal Information */}
<Grid item xs={12}>
  <Typography variant="h6" sx={{ mb: 2 }}>
    Personal Information
  </Typography>
</Grid>

<Grid item xs={12} sm={6}>
  <TextField
    fullWidth
    required
    id="name"
    name="name"
    label="Full Name"
    value={formData.name}
    onChange={handleChange}
    variant="outlined"
  />
</Grid>

<Grid item xs={12} sm={6}>
  <TextField
    fullWidth
    required
    id="email"
    name="email"
    label="Email Address"
    type="email"
    value={formData.email}
    onChange={handleChange}
    variant="outlined"
  />
</Grid>

<Grid item xs={12} sm={6}>
  <TextField
    fullWidth
    id="phone"
    name="phone"
    label="Phone Number"
    value={formData.phone}
    onChange={handleChange}
    variant="outlined"
  />
</Grid>

<Grid item xs={12} sm={6}>
  <TextField
    fullWidth
    required
    id="service"
    name="service"
    select
    label="Service"
    value={formData.service}
    onChange={handleChange}
    variant="outlined"
  >
    {services.map((service) => (
      <MenuItem key={service} value={service}>
        {service}
      </MenuItem>
    ))}
  </TextField>
</Grid>

These fields collect the user's basic information and the service they're interested in booking.

Step 3: Implement Date and Time Selection

Now, let's add the date and time pickers to our form:

{/* Date and Time Selection */}
<Grid item xs={12}>
  <Typography variant="h6" sx={{ mt: 2, mb: 2 }}>
    Date and Time
  </Typography>
</Grid>

<Grid item xs={12} sm={6}>
  <DatePicker
    label="Appointment Date"
    value={formData.date}
    onChange={handleDateChange}
    disablePast
    sx={{ width: '100%' }}
    slotProps={{
      textField: {
        required: true,
        fullWidth: true,
      },
    }}
  />
</Grid>

<Grid item xs={12} sm={6}>
  <TimePicker
    label="Appointment Time"
    value={formData.time}
    onChange={handleTimeChange}
    minutesStep={15}
    sx={{ width: '100%' }}
    slotProps={{
      textField: {
        required: true,
        fullWidth: true,
      },
    }}
  />
</Grid>

Here, we've added:

  1. A DatePicker that only allows future dates
  2. A TimePicker with 15-minute intervals for easier scheduling

Step 4: Add Notes and Submit Button

Let's complete our form with a notes field and submit button:

{/* Additional Notes */}
<Grid item xs={12}>
  <TextField
    fullWidth
    id="notes"
    name="notes"
    label="Additional Notes"
    multiline
    rows={4}
    value={formData.notes}
    onChange={handleChange}
    variant="outlined"
  />
</Grid>

{/* Submit Button */}
<Grid item xs={12}>
  <Button
    type="submit"
    variant="contained"
    size="large"
    disabled={isSubmitting}
    sx={{ mt: 2 }}
  >
    {isSubmitting ? 'Booking...' : 'Book Appointment'}
  </Button>
</Grid>

Step 5: Add Validation and Business Rules

Let's enhance our form with validation and business rules to ensure we collect valid data:

import React, { useState, useEffect } from 'react';
// ... other imports

const BookingForm = () => {
  // ... existing state
  
  // Add validation state
  const [errors, setErrors] = useState({});
  
  // Business hours
  const businessHours = {
    start: 9, // 9 AM
    end: 17,  // 5 PM
  };
  
  // Validate time is within business hours
  const validateTime = (time) => {
    const hour = time.hour();
    return hour >= businessHours.start && hour < businessHours.end;
  };
  
  // Validate form on change
  useEffect(() => {
    const newErrors = {};
    
    // Email validation
    if (formData.email && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i.test(formData.email)) {
      newErrors.email = 'Invalid email address';
    }
    
    // Phone validation (optional field)
    if (formData.phone && !/^[0-9]{10}$/.test(formData.phone.replace(/[^0-9]/g, ''))) {
      newErrors.phone = 'Phone should be 10 digits';
    }
    
    // Time validation
    if (formData.time && !validateTime(formData.time)) {
      newErrors.time = `Appointments are only available between ${businessHours.start}AM and ${businessHours.end}PM`;
    }
    
    setErrors(newErrors);
  }, [formData]);
  
  // Check if form is valid
  const isFormValid = () => {
    // Required fields
    if (!formData.name || !formData.email || !formData.service || !formData.date || !formData.time) {
      return false;
    }
    
    // No validation errors
    return Object.keys(errors).length === 0;
  };
  
  // Updated handleSubmit
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (!isFormValid()) {
      setSubmitError(true);
      return;
    }
    
    setIsSubmitting(true);
    
    // Simulate API call
    setTimeout(() => {
      console.log('Form submitted:', formData);
      setIsSubmitting(false);
      setSubmitSuccess(true);
      // Reset form after successful submission
      setFormData({
        name: '',
        email: '',
        phone: '',
        service: '',
        date: dayjs(),
        time: dayjs().hour(9).minute(0),
        notes: '',
      });
    }, 1500);
  };
  
  // ... rest of the component
}

Now, let's update our form fields to display validation errors:

<Grid item xs={12} sm={6}>
  <TextField
    fullWidth
    required
    id="email"
    name="email"
    label="Email Address"
    type="email"
    value={formData.email}
    onChange={handleChange}
    variant="outlined"
    error={!!errors.email}
    helperText={errors.email}
  />
</Grid>

<Grid item xs={12} sm={6}>
  <TextField
    fullWidth
    id="phone"
    name="phone"
    label="Phone Number"
    value={formData.phone}
    onChange={handleChange}
    variant="outlined"
    error={!!errors.phone}
    helperText={errors.phone}
  />
</Grid>

{/* ... */}

<Grid item xs={12} sm={6}>
  <TimePicker
    label="Appointment Time"
    value={formData.time}
    onChange={handleTimeChange}
    minutesStep={15}
    sx={{ width: '100%' }}
    slotProps={{
      textField: {
        required: true,
        fullWidth: true,
        error: !!errors.time,
        helperText: errors.time,
      },
    }}
    shouldDisableTime={(timeValue, view) => {
      if (view === 'hours' && (timeValue.hour() < businessHours.start || timeValue.hour() >= businessHours.end)) {
        return true;
      }
      return false;
    }}
  />
</Grid>

Step 6: Implement Business Rules for Date Selection

Let's add more sophisticated business rules for date selection, such as disabling weekends or specific dates:

// Disable weekends and holidays
const isWeekend = (date) => {
  const day = date.day();
  return day === 0 || day === 6; // 0 is Sunday, 6 is Saturday
};

// Example holidays (you would typically get these from a database or API)
const holidays = [
  dayjs('2023-12-25'), // Christmas
  dayjs('2023-12-31'), // New Year's Eve
  dayjs('2024-01-01'), // New Year's Day
];

const isHoliday = (date) => {
  return holidays.some(holiday => 
    holiday.format('YYYY-MM-DD') === date.format('YYYY-MM-DD')
  );
};

// Check if a date should be disabled
const shouldDisableDate = (date) => {
  return isWeekend(date) || isHoliday(date);
};

Now, update the DatePicker:

<DatePicker
  label="Appointment Date"
  value={formData.date}
  onChange={handleDateChange}
  disablePast
  shouldDisableDate={shouldDisableDate}
  sx={{ width: '100%' }}
  slotProps={{
    textField: {
      required: true,
      fullWidth: true,
    },
  }}
/>

Step 7: Add Date and Time Availability Checking

In a real booking system, you'd want to check if a specific date and time slot is available. Let's simulate this:

// Simulated booked slots (in a real app, this would come from your backend)
const bookedSlots = [
  { date: '2023-11-15', time: '10:00' },
  { date: '2023-11-15', time: '11:00' },
  { date: '2023-11-16', time: '14:00' },
  { date: '2023-11-17', time: '09:00' },
];

// Check if a time slot is available
const isTimeSlotAvailable = (date, time) => {
  const dateStr = date.format('YYYY-MM-DD');
  const timeStr = time.format('HH:mm');
  
  return !bookedSlots.some(
    slot => slot.date === dateStr && slot.time === timeStr
  );
};

// Add a function to show available slots
const getAvailableTimeSlots = (date) => {
  const slots = [];
  const dateStr = date.format('YYYY-MM-DD');
  
  // Generate all possible slots
  for (let hour = businessHours.start; hour < businessHours.end; hour++) {
    for (let minute = 0; minute < 60; minute += 15) {
      const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
      
      // Check if slot is available
      const isAvailable = !bookedSlots.some(
        slot => slot.date === dateStr && slot.time === timeStr
      );
      
      if (isAvailable) {
        slots.push({
          hour,
          minute,
          label: dayjs().hour(hour).minute(minute).format('h:mm A')
        });
      }
    }
  }
  
  return slots;
};

Now, let's modify our form to show available time slots based on the selected date:

// Add state for available time slots
const [availableTimeSlots, setAvailableTimeSlots] = useState([]);

// Update available time slots when date changes
useEffect(() => {
  if (formData.date) {
    setAvailableTimeSlots(getAvailableTimeSlots(formData.date));
  }
}, [formData.date]);

// In the render function, replace the TimePicker with:
<Grid item xs={12} sm={6}>
  <TextField
    select
    fullWidth
    required
    id="timeSlot"
    name="timeSlot"
    label="Available Time Slots"
    value={formData.time ? formData.time.format('HH:mm') : ''}
    onChange={(e) => {
      const [hour, minute] = e.target.value.split(':').map(Number);
      setFormData({
        ...formData,
        time: dayjs().hour(hour).minute(minute)
      });
    }}
    variant="outlined"
    error={!!errors.time}
    helperText={errors.time || 'Select an available time slot'}
  >
    {availableTimeSlots.length > 0 ? (
      availableTimeSlots.map((slot) => (
        <MenuItem 
          key={`${slot.hour}:${slot.minute}`} 
          value={`${slot.hour.toString().padStart(2, '0')}:${slot.minute.toString().padStart(2, '0')}`}
        >
          {slot.label}
        </MenuItem>
      ))
    ) : (
      <MenuItem disabled value="">
        No available slots for this date
      </MenuItem>
    )}
  </TextField>
</Grid>

Step 8: Add a Date Range Option for Multi-Day Bookings

For services that might span multiple days, let's add a date range option:

// Add to state
const [isMultiDay, setIsMultiDay] = useState(false);
const [dateRange, setDateRange] = useState([dayjs(), dayjs().add(1, 'day')]);

// Add UI for toggling multi-day booking
<Grid item xs={12}>
  <FormControlLabel
    control={
      <Switch
        checked={isMultiDay}
        onChange={(e) => setIsMultiDay(e.target.checked)}
        name="multiDay"
        color="primary"
      />
    }
    label="Multi-day booking"
  />
</Grid>

{/* Conditional rendering based on booking type */}
{isMultiDay ? (
  <Grid item xs={12}>
    <DateRangePicker
      startText="Check-in"
      endText="Check-out"
      value={dateRange}
      onChange={(newValue) => {
        setDateRange(newValue);
      }}
      disablePast
      shouldDisableDate={shouldDisableDate}
      sx={{ width: '100%' }}
    />
  </Grid>
) : (
  <Grid item xs={12} sm={6}>
    <DatePicker
      label="Appointment Date"
      value={formData.date}
      onChange={handleDateChange}
      disablePast
      shouldDisableDate={shouldDisableDate}
      sx={{ width: '100%' }}
      slotProps={{
        textField: {
          required: true,
          fullWidth: true,
        },
      }}
    />
  </Grid>
)}

Note: Make sure to import the necessary components:

import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
import { FormControlLabel, Switch } from '@mui/material';

Step 9: Add Visual Feedback and Summary

Let's enhance our form with visual feedback and a booking summary:

// Add state for showing summary
const [showSummary, setShowSummary] = useState(false);

// Add to the form, before the submit button
<Grid item xs={12}>
  <Paper 
    elevation={1} 
    sx={{ 
      p: 2, 
      mt: 2, 
      mb: 2, 
      bgcolor: 'primary.light', 
      color: 'primary.contrastText',
      display: showSummary ? 'block' : 'none'
    }}
  >
    <Typography variant="h6" gutterBottom>
      Booking Summary
    </Typography>
    <Typography variant="body1">
      <strong>Name:</strong> {formData.name}
    </Typography>
    <Typography variant="body1">
      <strong>Service:</strong> {formData.service}
    </Typography>
    <Typography variant="body1">
      <strong>Date:</strong> {formData.date.format('dddd, MMMM D, YYYY')}
    </Typography>
    <Typography variant="body1">
      <strong>Time:</strong> {formData.time.format('h:mm A')}
    </Typography>
  </Paper>
</Grid>

{/* Update the submit button section */}
<Grid item xs={12} container spacing={2} justifyContent="space-between">
  <Grid item>
    <Button
      variant="outlined"
      onClick={() => setShowSummary(!showSummary)}
    >
      {showSummary ? 'Hide Summary' : 'Preview Booking'}
    </Button>
  </Grid>
  <Grid item>
    <Button
      type="submit"
      variant="contained"
      size="large"
      disabled={isSubmitting || !isFormValid()}
      sx={{ mt: 2 }}
    >
      {isSubmitting ? 'Booking...' : 'Book Appointment'}
    </Button>
  </Grid>
</Grid>

Advanced Capabilities and Customizations

Now that we have a functional booking form, let's explore some advanced capabilities and customizations for MUI date and time pickers.

Custom Date Formats

You can customize the date format displayed in the input field:

<DatePicker
  label="Custom Format Date"
  value={formData.date}
  onChange={handleDateChange}
  format="MMMM D, YYYY" // Shows as "November 15, 2023"
  sx={{ width: '100%' }}
  slotProps={{
    textField: {
      fullWidth: true,
    },
  }}
/>

Custom Styling with Theme Overrides

You can customize the appearance of the date picker using theme overrides:

const theme = createTheme({
  components: {
    MuiPickersDay: {
      styleOverrides: {
        root: {
          fontSize: '0.9rem',
          '&.Mui-selected': {
            backgroundColor: '#2e7d32',
            '&:hover': {
              backgroundColor: '#1b5e20',
            },
          },
        },
        today: {
          border: '1px solid #2e7d32',
        },
      },
    },
    MuiClock: {
      styleOverrides: {
        pin: {
          backgroundColor: '#2e7d32',
        },
        clock: {
          backgroundColor: '#f1f8e9',
        },
      },
    },
  },
});

Inline Calendar Display

For a more user-friendly experience, you can display the calendar inline:

import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker';

// In your component
<Grid item xs={12}>
  <Paper elevation={1} sx={{ p: 2 }}>
    <Typography variant="h6" gutterBottom>
      Select Date
    </Typography>
    <StaticDatePicker
      displayStaticWrapperAs="desktop"
      value={formData.date}
      onChange={handleDateChange}
      disablePast
      shouldDisableDate={shouldDisableDate}
      renderInput={(params) => <TextField {...params} />}
    />
  </Paper>
</Grid>

Custom Day Rendering

You can customize how days are rendered in the calendar:

<DatePicker
  label="Appointment Date"
  value={formData.date}
  onChange={handleDateChange}
  disablePast
  shouldDisableDate={shouldDisableDate}
  slots={{
    day: (props) => {
      // Add price or availability information to specific dates
      const date = props.day;
      const dateStr = date.format('YYYY-MM-DD');
      
      // Example: Add pricing information to dates
      const prices = {
        '2023-11-20': '$50',
        '2023-11-21': '$45',
        '2023-11-22': '$60',
      };
      
      const price = prices[dateStr];
      
      return (
        <div
          {...props}
          style={{
            ...props.style,
            position: 'relative',
            height: '40px',
          }}
        >
          <span>{date.date()}</span>
          {price && (
            <span style={{
              position: 'absolute',
              bottom: 2,
              left: 0,
              right: 0,
              fontSize: '0.6rem',
              textAlign: 'center',
              color: 'green',
            }}>
              {price}
            </span>
          )}
        </div>
      );
    },
  }}
  sx={{ width: '100%' }}
  slotProps={{
    textField: {
      required: true,
      fullWidth: true,
    },
  }}
/>

Integration with Form Libraries

MUI date pickers can be easily integrated with form libraries like Formik or React Hook Form. Here's an example with Formik:

import { useFormik } from 'formik';
import * as Yup from 'yup';

// Validation schema
const validationSchema = Yup.object({
  name: Yup.string().required('Name is required'),
  email: Yup.string().email('Invalid email address').required('Email is required'),
  service: Yup.string().required('Service is required'),
  date: Yup.date().required('Date is required'),
  time: Yup.date().required('Time is required'),
});

// In your component
const formik = useFormik({
  initialValues: {
    name: '',
    email: '',
    phone: '',
    service: '',
    date: dayjs(),
    time: dayjs().hour(9).minute(0),
    notes: '',
  },
  validationSchema,
  onSubmit: (values) => {
    console.log('Form submitted:', values);
    // Handle form submission
  },
});

// In your JSX
<form onSubmit={formik.handleSubmit}>
  {/* ... other fields ... */}
  
  <Grid item xs={12} sm={6}>
    <DatePicker
      label="Appointment Date"
      value={formik.values.date}
      onChange={(newValue) => {
        formik.setFieldValue('date', newValue);
      }}
      disablePast
      shouldDisableDate={shouldDisableDate}
      sx={{ width: '100%' }}
      slotProps={{
        textField: {
          required: true,
          fullWidth: true,
          error: formik.touched.date && Boolean(formik.errors.date),
          helperText: formik.touched.date && formik.errors.date,
        },
      }}
    />
  </Grid>
  
  {/* ... other fields ... */}
</form>

Best Practices and Common Issues

When working with MUI date and time pickers in a booking form, here are some best practices and common issues to be aware of:

Best Practices

  1. Always provide clear labels and helper text

Ensure your date and time pickers have clear labels and, when necessary, helper text to guide users:

<DatePicker
  label="Appointment Date"
  slotProps={{
    textField: {
      helperText: "Select a weekday for your appointment",
    },
  }}
  // other props
/>
  1. Handle timezones appropriately

When working with dates and times, always consider timezone issues:

// Store timezone information with the date
const handleSubmit = (e) => {
  e.preventDefault();
  
  // Get user's timezone
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  
  // Include timezone in submission data
  const submissionData = {
    ...formData,
    timezone,
    // Convert to ISO string for backend storage
    dateTimeISO: dayjs(formData.date)
      .hour(formData.time.hour())
      .minute(formData.time.minute())
      .toISOString(),
  };
  
  // Submit data
  console.log('Submitting with timezone info:', submissionData);
};
  1. Provide reasonable defaults

Always set reasonable default values for your date and time pickers:

// Set default to next business day at 9 AM
const getNextBusinessDay = () => {
  let date = dayjs().add(1, 'day');
  
  // Skip to Monday if it's Friday or weekend
  const day = date.day();
  if (day === 5) { // Friday
    date = date.add(3, 'day');
  } else if (day === 6) { // Saturday
    date = date.add(2, 'day');
  } else if (day === 0) { // Sunday
    date = date.add(1, 'day');
  }
  
  return date;
};

// Use in initial state
const [formData, setFormData] = useState({
  // other fields...
  date: getNextBusinessDay(),
  time: dayjs().hour(9).minute(0),
});
  1. Optimize for mobile

Ensure your date and time pickers work well on mobile devices:

<DatePicker
  label="Appointment Date"
  value={formData.date}
  onChange={handleDateChange}
  disablePast
  // Optimize for mobile
  desktopModeMediaQuery="@media (min-width: 768px)"
  // other props
/>

Common Issues and Fixes

  1. Date formatting inconsistencies

Problem: Different locales display dates differently, causing confusion.

Solution: Explicitly set the format and use the LocalizationProvider with the appropriate locale:

import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import 'dayjs/locale/fr'; // Import the locale you need

// In your App.js or component
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="fr">
  {/* Your components */}
</LocalizationProvider>
  1. Time picker showing 24-hour format when 12-hour is expected

Problem: Time format doesn't match user expectations.

Solution: Explicitly set the time format:

<TimePicker
  label="Appointment Time"
  value={formData.time}
  onChange={handleTimeChange}
  ampm={true} // Use 12-hour format with AM/PM
  // other props
/>
  1. Date validation errors

Problem: Users can still select invalid dates despite validation.

Solution: Combine built-in validation with custom validation:

// Combine multiple validation rules
const shouldDisableDate = (date) => {
  // Check if weekend
  if (isWeekend(date)) return true;
  
  // Check if holiday
  if (isHoliday(date)) return true;
  
  // Check if fully booked
  if (isFullyBooked(date)) return true;
  
  return false;
};

// Use in DatePicker
<DatePicker
  shouldDisableDate={shouldDisableDate}
  // other props
/>
  1. Performance issues with many disabled dates

Problem: Calculating many disabled dates can cause performance issues.

Solution: Optimize your validation functions:

// Use memoization for expensive calculations
import { useMemo } from 'react';

// In your component
const bookedDates = useMemo(() => {
  // This calculation only runs when dependencies change
  return calculateBookedDates(availabilityData);
}, [availabilityData]);

// Use a more efficient check
const shouldDisableDate = useCallback((date) => {
  const dateStr = date.format('YYYY-MM-DD');
  return bookedDates.has(dateStr); // Using a Set for O(1) lookup
}, [bookedDates]);
  1. Form submission with incorrect date/time format

Problem: Dates are submitted in an unexpected format.

Solution: Format dates consistently before submission:

const handleSubmit = (e) => {
  e.preventDefault();
  
  // Create a consistent date-time object
  const appointmentDateTime = dayjs(formData.date)
    .hour(formData.time.hour())
    .minute(formData.time.minute())
    .second(0);
  
  const formattedData = {
    ...formData,
    // Format for display
    formattedDateTime: appointmentDateTime.format('YYYY-MM-DD HH:mm'),
    // ISO string for API
    isoDateTime: appointmentDateTime.toISOString(),
  };
  
  console.log('Submitting:', formattedData);
  // Submit to API
};

Wrapping Up

In this comprehensive guide, we've explored how to build a fully functional booking form using MUI's date and time picker components. We've covered everything from basic setup to advanced customizations, validation, and integration with form libraries.

The MUI date and time pickers offer powerful functionality out of the box while remaining highly customizable. By following the patterns and practices outlined in this article, you can create user-friendly booking experiences that handle complex business rules and validation requirements.

Remember to always consider the user experience when implementing date and time selection interfaces. Clear labels, helpful guidance, and intuitive defaults go a long way toward creating a smooth booking process. Additionally, proper validation and error handling ensure that users can't submit invalid bookings, saving time and frustration for both users and administrators.