How to Create a Custom Select Field in Payload: A Step-by-Step Guide

Create a Custom Select field in Payload
This quick tutorial will guide you through the process of creating a custom select component, demonstrating how to override default UI components, how to import options from an external API, and storing your custom values.

Let's jump right in and start building a custom select field that you can use in Payload.

The standard select field allows you to choose from multiple options, and these options are predefined and passed directly to the select field.

What we're going to build is a select field that looks and works the same but has its options imported from an external source.

This can help you integrate data from a third party. For example, it might be a list of form IDs or product IDs. It's also really useful when you need to display large, universal data such as countries or national holidays.

First, we want to define our base field.

import { Field } from 'payload/types';
import { CustomSelectComponent } from './component';
export const CustomSelectField: Field = {
name: 'customSelectField',
type: 'text',
admin: {
components: {
Field: CustomSelectComponent,
},
}
}

Although we're building a select component, it's important to note that we won't be using the select type. The select type field's underlying structure is an enum and must be predefined. Since this custom field will use external options, that approach won't work.

Instead, we're going to use the type text. Then we want to override the front-end field. To do this, we'll pass in our own component to the admin components field.

Now, let's get into the custom component.

import * as React from 'react';
import { SelectInput, useField } from 'payload/components/forms';
export const CustomSelectComponent: React.FC<{ path: string }> = ({ path }) => {
const { value, setValue } = useField<string>({ path });
const [options, setOptions] = React.useState([]);
// Fetch options on component mount
React.useEffect(() => {
const fetchOptions = async () => {
try {
const response = await fetch('https://restcountries.com/v3.1/all');
const data = await response.json();
const countryOptions = data.map((country) => {
return {
label: `${country.name.common + ' ' + country.flag}`,
value: country.name.common,
};
});
setOptions(countryOptions.sort(
(a, b) => a.label.localeCompare(b.label)
));
} catch (error) {
console.error('Error fetching data:', error);
}
};
return (
<div>
<label className='field-label'>
Custom Select - Countries
</label>
<SelectInput
path={path}
name={path}
options={options}
value={value}
onChange={(e) => setValue(e.value)}
/>
</div>
)
fetchOptions();
}, []);
};

Essentially, what we're doing here is replicating the existing Payload select field and then passing in our own options – simple as that!

The first thing we're going to do is import the existing select component directly from Payload. After that, we'll want to output this component ourselves.

The select component will require the path, name, your options, and value.

We can also import the useField function from Payload, which will help us get and set the value.

The field path can be de-structured directly off the component and then passed in to our useField function. From that function, we can de-structure value and also a setValue function.

So, if you go and take another look, you can see where the path is coming from, along with the value and setValue.

return (
<div>
<label className='field-label'>
Custom Select - Countries
</label>
<SelectInput
path={path}
name={path}
options={options}
value={value}
onChange={(e) => setValue(e.value)}
/>
</div>
)

Now onto our options.

The first thing we want to do is define an empty state. Then, we come down to our fetchOptions async function.

Essentially, what this function needs to do is pull in your data and then restructure the shape of data so that you can output it as an array of objects, each with a label and a value.

To start, it's making a fetch request to the restcountries.com API, and then we wait for that response to be converted into JSON.

Next, we're going to map over that JSON data and return a label and value for each country.

React.useEffect(() => {
const fetchOptions = async () => {
try {
const response = await fetch('https://restcountries.com/v3.1/all');
const data = await response.json();
const countryOptions = data.map((country) => {
return {
label: `${country.name.common + ' ' + country.flag}`,
value: country.name.common,
};
});
setOptions(countryOptions.sort(
(a, b) => a.label.localeCompare(b.label)
));
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchOptions();
}, []);

Now, we're back to the useState setOptions function.

Within the function, I've added a couple lines of code to put the countries in alphabetical order. But essentially, we're just setting those options back into our options state.

It's also important that we wrap all of this in a try catch so that if anything goes wrong, we won't break the whole admin panel.

Finally, we only want to run this function once. So, we're running this function inside of a useEffect with no dependencies, which means this will only run once when the component initially mounts.

One last tip: you can add the class name field-label, and Payload will automatically apply styles to this so that it matches everything else.

And that is it!

A custom select field with a dropdown list of countries, in the typical Payload style

Earlier, we talked about a regular select, and now appearing just the same is our nice, new custom select with all the countries - and if take a look at the data, you will see the new `customField` and its value.

{
"id": "64dele85d24a52f3cbc179e2",
"customSelectField": "Poland",
"createdAt": "2003-08-17T13:20:05.661Z",
"updatedAt": "2023-08-17T03:22:20.766Z"
}

Here is a link to the GitHub repo containing this code .

If you have any questions, be sure to join us on Discord!