Last updated February 06, 2023

Creating a custom input to display and save third party data

By Simeon Griggs

Sometimes your content lives outside Sanity, but you need to store and reference it from the inside of the Content Lake.

For this, we can create a custom input component which queries an API, populates choices in a dropdown, and saves the value into a document.

If your content creators are currently copy-and-pasting values from an external service into a field, this guide is for you!

Any service with a queryable API can be turned into a dynamic set of options for a custom input.

Possible use cases include:

  • The ID of a contact form from HubSpot
  • The ID of an open position from Lever
  • The SKU of a product from your ecommerce provider

Walkthrough

Before you begin...

There's so much you can do with form components, so if you'd like to go deeper and learn more, read the docs.

Also, you should consider the value saved in this Component similar to a weak reference. If the value you retrieved from your API is removed from that service, your Sanity content will still contain that value, but it will be outdated.

Let's start!

In the code example below is a schema for a document type named product, and within it, a string field named sku with a placeholder React Component for the input.

Create a string field just like this sku field and register it to your schema.

// ./schemas/product.ts
// Add this to the schema types registered in your sanity.config.ts file

import { defineField, defineType } from "sanity";

import ProductSelector from '../components/ProductSelector'

export default defineType({
  name: 'product',
  title: 'Product',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
    }),
    defineField({
      name: 'sku',
      title: 'SKU',
      type: 'string',
      components: {
        input: ProductSelector,
      },
    }),
  ],
})

Separately, create a file for the ProductSelector component:

// ./components/ProductSelector.tsx

export default function ProductSelector() {
  return <div>ProductSelector</div>
}

Performing a fetch request inside a React component can be tricky. An asynchronous function needs to be run inside a useEffect() hook, or you might install a fully-featured querying package like TanStack Query or SWR.

For simplicity in this tutorial, you'll need to install the package usehooks-ts for its useFetch() React Hook. It makes performing a fetch easy and provides loading and error states.

From the command line, in your Studio folder, run:

npm install usehooks-ts

Data Fetching

For this example, you're going to use fakestoreapi.com for products, but you could add any publicly queryable API here. A third-party API is likely to need some authentication, and you will need to adjust the demo to suit.

Gotcha

Suppose your targeted API does not have configurable CORS settings to allow fetch requests from inside the Studio. In that case, you may also need to "wrap" your request in a serverless function to authenticate and return the data you need.

Your next step is adding data fetching, error, and loading states to our component. Plus, a nice confirmation of success – to make sure everything is working.

// ./components/ProductSelector.tsx

import {useFetch} from 'usehooks-ts'
import {Card, Spinner, Text} from '@sanity/ui'

const url = 'https://fakestoreapi.com/products'
const cardProps = {shadow: 1, padding: 3, radius: 2}

export default function ProductSelector() {
  const {data, error} = useFetch<any[]>(url)

  if (error)
    return (
      <Card tone="critical" {...cardProps}>
        <Text>There has been an error</Text>
      </Card>
    )

  if (!data)
    return (
      <Card tone="default" {...cardProps}>
        <Spinner />
      </Card>
    )

  return (
    <Card tone="positive" {...cardProps}>
      <Text>Success! {data.length} Products Found</Text>
    </Card>
  )
}

To see if this is working, you should now see a green box announcing "Success! 20 Products Found".

Success, your API request has returned data!

This is great! But, not very useful.

Let's make something useful.

Add the Select component to the imports from @sanity/ui and map over the data to create a dropdown menu showing the title and price of each item – but with the item's id as the value is saved to the document.

// ./components/ProductSelector.tsx

// Update the imports to include Select
import {Select, Card, Spinner, Text} from '@sanity/ui'

// ... rest of the code

// Update the last return in the component
return (
  <Select>
    {data.map((item) => (
      <option key={item.id} value={item.id}>
        {item.title} (${item.price})
      </option>
    ))}
  </Select>
)

You should now have a select menu with options from the API.

Currently, selecting an item doesn't do anything

Now you've got a selectable select menu, but it just doesn't write to the document

Using the code below, your new form component can patch the document each time the select menu is changed to save the item id to the document in the sku field.

Here's the final input component code:

// ./components/ProductSelector.tsx

import React from 'react'
import {StringInputProps, set, unset} from 'sanity'
import {useFetch} from 'usehooks-ts'
import {Select, Card, Spinner, Text} from '@sanity/ui'

const url = 'https://fakestoreapi.com/products'
const cardProps = {shadow: 1, padding: 3, radius: 2}

export default function ProductSelector(props: StringInputProps) {
  // The onChange handler can write new changes to the field
  const {onChange, value} = props
  const {data, error} = useFetch<any[]>(url)

  // This function will run each time the select menu is used
  const handleChange = React.useCallback(
    (event: React.FormEvent<HTMLSelectElement> | undefined) => {
      const value = event?.currentTarget.value

      // If the selected option has a value,
      // it will be written to the document
      // otherwise the field will be cleared
      onChange(value ? set(value) : unset())
    },
    [onChange]
  )

  if (error)
    return (
      <Card tone="critical" {...cardProps}>
        <Text>There has been an error</Text>
      </Card>
    )

  if (!data)
    return (
      <Card tone="default" {...cardProps}>
        <Spinner />
      </Card>
    )

  return (
    <Select onChange={handleChange} value={value}>
      {data.map((item) => (
        <option key={item.id} value={item.id}>
          {item.title} (${item.price})
        </option>
      ))}
    </Select>
  )
}

Create a new document, select an option, and you should see an ID number written to the sku field.

Our custom input looks Studio-ready and writes to the document

Improvements from here

There's much more you can do to improve the editing experience.

Some examples:

  1. Add a button to "unset" the field so editors can remove the value from the document.
  2. Setup the component to read the URL from options on the string field for a more composable custom input.
  3. Render the default input below the select menu using {props.renderDefault(props)}
  4. Save needless API requests on page load by having a "Fetch" button to populate the Select menu manually.
  5. In addition, perhaps don't Fetch from the API if the field already has a value – but Fetch again if the value has been removed.
  6. Swap out <Select> for the more powerful Sanity UI <Autocomplete> component for more intelligent searching and visual previews of returned data.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by author