React Hustle

How to Create a Custom Formik Dropdown Component

JA

Jasser Mark Arioste

How to Create a Custom Formik Dropdown Component

Hello hustlers! In this guide, you’ll learn how to create a custom Formik Dropdown component using HeadlessUI, DaisyUI, and TailwindCSS.

To create a custom formik dropdown component, we’ll first create a regular dropdown component and create a wrapper component to add the formik functionality.

What Makes a Good Dropdown?

First, let’s ask ourselves, what makes a good dropdown component? This will be our guide to implementing the component. For me, it should have the following:

  1. It should be written in Typescript for type safety. We’ll use NextJS with Typescript to achieve this.
  2. The component should look professional. Here, We’ll use DaisyUI + TailwindCSS to style the component.
  3. The component should be accessible and respond to keyboard input. We’ll use HeadlessUI to easily create accessible components.
  4. It should use Typescript generics to accept any kind of value for the options. We’ll create a generic component to achieve this.

You can add or remove requirements depending on your project or situation.

Final Output

Here’s a simple demo of the component we’ll be making. You can also access the demo at Vercel .

Step 1 - Project Setup

If you want to follow along step-by-step, I created a GitHub repo that includes all the dependencies: @headlessui/react , formik , daisyui , and tailwindcss . You can easily set up this NextJS command by using the commands:

npx create-next-app -e https://github.com/jmarioste/formik-dropdown-example formik-dropdown-example
cd formik-dropdown-example && yarn dev

This will set up a local server in localhost:3000 where you’ll be greeted by this screen:

Step 2 - Defining a Generic Dropdown Component

Let’s create a generic component so that our dropdown can hold any kind of value such as string , number , boolean or object .

First, create a file in components/CustomDropdown.tsx .

// components/CustomDropdown.tsx
import React from "react";

const CustomDropdown = () => {
  return <div>CustomDropdown</div>;
};

export default CustomDropdown;

Next, Let’s add the generic Props for our component:

import React from "react";
//👇 generic type for dropdown option
export type CustomDropdownOption<T> = {
  label: string;
  value: T;
};
//👇 generic props
export type CustomDropdownProps<T> = {
  options: CustomDropdownOption<T>[];
  value: T;
  onChange(value: T): void;
};
//👇 generic dropdown component
const CustomDropdown = <T,>(props: CustomDropdownProps<T>) => {
  return <div>CustomDropdown</div>;
};
export default CustomDropdown;

Now, we can use our dropdown component like the one below. Notice that the values for the options are different for each dropdown. This is the beauty of generic components:

// pages/index.tsx
import { NextPage } from "next";
import React from "react";
import { Inter } from "@next/font/google";
import CustomDropdown from "components/CustomDropdown";
const inter = Inter();
const HomePage: NextPage = () => {
  return (
    <div className={"container " + inter.className}>
      <div className="grid place-content-center min-h-screen">
        <div>
          <h1>Welcome to Formik Dropdown Example</h1>
          {/* 👇 Uses string type for the value */}
          <CustomDropdown
            options={[
              {
                label: "Software Engineer",
                value: "Software Engineer",
              },
              {
                label: "React Developer",
                value: "React Developer",
              },
            ]}
            value=""
            onChange={(value) => {
              console.log(value);
            }}
          />
          {/*👇 Uses number type for the value */}
          <CustomDropdown
            options={[
              {
                label: "$2000/month",
                value: 2000,
              },
              {
                label: "$4000/month",
                value: 4000,
              },
            ]}
            value={0}
            onChange={(value) => {
              console.log(value);
            }}
          />
        </div>
      </div>
    </div>
  );
};
export default HomePage;

If you want to know more about generic components, or useful patterns in react and typescript, check this guide by React Typescript Cheatsheet .

Step 3 - Adding Functionality using HeadlessUI

Now that we’ve defined how to use the component, let’s create the actual component itself. This will be simple since we have all the functionality we need using the HeadlessUI package.

// components/CustomDropdown.tsx
...
import { Listbox } from "@headlessui/react";
...
const CustomDropdown = <T,>(props: CustomDropdownProps<T>) => {
  const options = props.options;
  const selectedItem = options.find((o) => o.value === props.value);
  const label = selectedItem?.label ?? "Select Option...";
  return (
    <Listbox value={props.value} onChange={props.onChange}>
      <Listbox.Button>{label}</Listbox.Button>
      <Listbox.Options>
        {options.map((option, i) => (
          <Listbox.Option key={i} value={option.value}>
            {option.label}
          </Listbox.Option>
        ))}
      </Listbox.Options>
    </Listbox>
  );
};
...

Now, let’s modify pages/index.tsx and add useState to track the value for each dropdown:

import React, { useState } from "react";
...
const HomePage: NextPage = () => {
  const [val, setVal] = useState("");
  const [val2, setVal2] = useState(0);
  return (
    ...
    <CustomDropdown
      options={[
        {
          label: "Software Engineer",
          value: "Software Engineer",
        },
        {
          label: "React Developer",
          value: "React Developer",
        },
      ]}
      value={val}
      onChange={(value) => {
        setVal(value);
      }}
    />
    <CustomDropdown
      options={[
        {
          label: "$2000/month",
          value: 2000,
        },
        {
          label: "$4000/month",
          value: 4000,
        },
      ]}
      value={val2}
      onChange={(value) => {
        setVal2(value);
      }}
    />;
    ... 
  )
}
...

Here’s what our component looks like after this step. It looks pretty bad but we got the basic functionality:

Step 4 - Adding Styling Using DaisyUI Classes

Next, we’ll be adding styling to our custom dropdown. We’ll also use render props to access the state of the ListBox.Option component from HeadlessUI. The available state from render props for the ListBox is written in HeadlessUI docs :

// components/CustomDropdown.tsx
...
import cn from "classnames";
...
import React from "react";
import { Listbox } from "@headlessui/react";
import cn from "classnames";
//👇 generic type for dropdown option
export type CustomDropdownOption<T> = {
  label: string;
  value: T;
};
//👇 generic props
export type CustomDropdownProps<T> = {
  options: CustomDropdownOption<T>[];
  value: T;
  onChange(value: T): void;
};
//👇 generic dropdown component
const CustomDropdown = <T,>(props: CustomDropdownProps<T>) => {
  const options = props.options;
  const selectedItem = options.find((o) => o.value === props.value);
  const label = selectedItem?.label ?? "Select Option...";
  return (
    <Listbox value={props.value} onChange={props.onChange} as={React.Fragment}>
      <div className={"dropdown dropdown-end w-full"}>
        {/* 👇 Classes for button */}
        <Listbox.Button className="btn btn-outline w-full relative no-animation normal-case">
          {label}
        </Listbox.Button>
        {/* 👇 Classes for options */}
        <Listbox.Options
          className={cn({
            "dropdown-content menu": true,
            "p-2 shadow-lg bg-base-100 rounded-box w-72": true,
          })}
        >
          {options.map((option, i) => (
            <Listbox.Option key={i} value={option.value}>
              {/* 👇 Use render props to get active, disabled and selected state */}
              {({ active, disabled, selected }) => (
                <button
                  className={cn({
                    active: selected,
                    "btn-disabled": disabled,
                    "bg-primary/80 text-primary-content": active,
                  })}
                >
                  {option.label}
                </button>
              )}
            </Listbox.Option>
          ))}
        </Listbox.Options>
      </div>
    </Listbox>
  );
};
export default CustomDropdown;

After this step, we get a beautiful custom dropdown component:

Step 5 - Adding Formik Functionality

To integrate Formik into any component, we can use useFormikContext and useField custom hooks that are provided by Formik.

First, Create a file components/FormikCustomDropdown.tsx

// components/FormikCustomDropdown.tsx
import { useField, useFormikContext } from "formik";
import React from "react";
import CustomDropdown, { CustomDropdownOption } from "./CustomDropdown";
type Props<T> = {
  name: string;
  options: CustomDropdownOption<T>[];
};
const FormikCustomDropdown = <T,>(props: Props<T>) => {
  const name = props.name;
  const [field] = useField<T>(name);
  const { setFieldValue } = useFormikContext();
  // 👇 listen to any change in value and use setFieldValue
  // to modify the formik context state
  const handleChange = (val: T) => {
    setFieldValue(name, val);
  };
  return (
    <CustomDropdown
      options={props.options}
      onChange={handleChange}
      value={field.value}
    />
  );
};
export default FormikCustomDropdown;

Step 6 - Using the Custom Formik Dropdown

To use the <FormikCustomDropdown/> component, we’ll have to use the <Formik/> provider component. Let’s modify our pages/index.tsx page to achieve this:

// pages/index.tsx
import { NextPage } from "next";
import React from "react";
import FormikCustomDropdown from "components/FormikCustomDropdown";
import { Formik, Form } from "formik";
const HomePage: NextPage = () => {
  return (
    <div className={"container"}>
      <div className="grid place-content-center min-h-screen">
        <Formik
          onSubmit={(values) => alert(JSON.stringify(values, null, 4))}
          initialValues={{
            position: "",
            salary: 0,
          }}
        >
          <Form className="flex flex-col gap-2">
            <h1>Welcome to Formik Dropdown Example</h1>
            <FormikCustomDropdown
              name="position"
              options={[
                {
                  label: "Software Engineer",
                  value: "Software Engineer",
                },
                {
                  label: "React Developer",
                  value: "React Developer",
                },
              ]}
            />
            <FormikCustomDropdown
              name="salary"
              options={[
                {
                  label: "$2000/month",
                  value: 2000,
                },
                {
                  label: "$4000/month",
                  value: 4000,
                },
              ]}
            />
            <button className="btn" type="submit">
              Submit Form
            </button>
          </Form>
        </Formik>
      </div>
    </div>
  );
};
export default HomePage;

Explanation:

We no longer use the useState hook since the formik context provides all the states for the form.

We added a name prop to both <FormikCustomDropdown/> components to correspond to the initial state.

We also added a button to submit the form.

That’s it!

Full Code and Demo

The full code can be accessed at GiHub: jmarioste/formik-dropdown-example . The demo can be accessed at Vercel .

Conclusion

We learned how to create a custom dropdown component using HeadlessUI and DaisyUI and integrate it into Formik by using Formik hooks.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter .

Credits: Image by Pexels from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle