Using Zod to validate date range picker

April 14, 2024 (8mo ago)

16 min read

What is Zod?

Prerequisites

I used shadcn/ui for the date range picker component, date-fns for the date format, and zod for the validation.

Getting started

Creating challenges on the Lotus project requires attention to detail, especially when setting the start and end dates. To simplify this process for others, I wrote a blog about creating a date range selection component for the challenge creation form.

First, I use Zod to create a new schema called dateRangeSchema. This schema has a date range object with the start date and end date of the form data.

import { z } from "zod";
export const dateRangeSchema = z
  .object({
    // other fields
    dateRange: z.object(
      {
        from: z.date(),
        to: z.date(),
      },
      {
        required_error: "Please select a date range",
      }
    ),
  })
  .refine((data) => data.dateRange.from < data.dateRange.to, {
    path: ["dateRange"],
    message: "From date must be before to date",
  });

Now that the Zod schema is ready, I will create the form step by step, starting with the necessary imports.

I have added the dateRangeSchema content to @/zod/schemas/date-range.ts. You can save it in the file if you want but don't forget to edit it if necessary.

import { cn } from "@/lib/utils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "../ui/form";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { dateRangeSchema } from "@/zod/schemas/date-range";

Schema and imports are done. Now I can create the component. When I create the component, I first define defaultValues and resolver using useForm.

  • defaultValues is a property that sets the initial values of the form fields when the form is first loaded or reset.
  • zodResolver is a function that converts a Zod schema into a form resolver, ensuring that the form data matches the schema before submission.
export const Component = () => {
  const form = useForm({
    defaultValues: {
      dateRange: {
        from: new Date(),
        to: new Date(),
      },
    },
    resolver: zodResolver(dateRangeSchema),
  });

  const onSubmit = (data: z.infer<typeof dateRangeSchema>) => {
    console.log(data);
  };

  return (
    // ...
  );
};

Now everything is ready. Finally, I include the date range picker in Component using shadcn/ui forms.

return (
  <Form {...form}>
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <FormField
        control={form.control}
        name="dateRange"
        render={({ field })=> (
          <FormItem className="flex flex-col">
            <FormLabel>Start and End Date</FormLabel>
            <Popover modal={true}>
              <PopoverTrigger asChild>
                <Button
                  id="date"
                  variant="outline"
                  className={cn(
                    "w-full justify-start text-left font-normal",
                    !field.value.from && "text-muted-foreground"
                  )}
                >
                  <CalendarIcon className="mr-2 h-4 w-4" />
                  {field.value.from ? (
                    field.value.to ? (
                      <>
                        {format(field.value.from, "LLL dd, y")} -{" "}
                        {format(field.value.to, "LLL dd, y")}
                      </>
                    ) : (
                      format(field.value.from, "LLL dd, y")
                    )
                  ) : (
                    <span>Pick a date</span>
                  )}
                </Button>
              </PopoverTrigger>
              <PopoverContent className="w-auto p-0" align="center">
                <Calendar
                  initialFocus
                  mode="range"
                  defaultMonth={field.value.from}
                  selected={{
                    from: field.value.from!,
                    to: field.value.to,
                  }}
                  onSelect={field.onChange}
                  numberOfMonths={2}
                />
              </PopoverContent>
            </Popover>
            <FormDescription>Select the start and end date</FormDescription>
            <FormMessage />
          </FormItem>
        )}
      />
    </form>
  </Form>
);

Now we have everything ready. To make the code better,

  • You can make Date Range Picker a component.
  • You can improve the onSubmit functionality of the form by using the Sonner package.
  • You can add custom error messages to the schema.

follow me on X.