Join us and panelists from Shopify, Figma, Loom, and Fnatic for the next Sanity.io Open House. Dec 8th.
November 03, 2021

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 editors 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

Before we begin...

You can do so much with custom input components so if you'd like to go deeper and learn more, make sure to read the docs.

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

Let's start!

Register a new string field in our schema along with a simple React Component. Through this guide we'll modify the Component to make it more featured.

The rest of the code examples will look only at this ProductSelector() component.

// product.js
// Put this in a folder with your other schemas and remember to register it!

import React from 'react'

function ProductSelector() {
  return <div>SKU Field</div>
}

export default {
  name: 'product',
  title: 'Product',
  type: 'document',
  fields: [
    {name: 'title', title: 'Title', type: 'string'},
    {
      name: 'sku',
      title: 'SKU',
      type: 'string',
      inputComponent: ProductSelector,
    },
  ],
}

Next we'll need some packages:

  • @sanity/ui to make our component's appearance suit the rest of the Studio and
  • usehooks-ts for its useFetch() React Hook to add some loading/error states to a Fetch request

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

npm install @sanity/ui usehooks-ts

Data Fetching

For this example we'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

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

Let's add data fetching, along with error and loading states to our card. Plus a nice confirmation of success – just to make sure everything is working.

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}

function ProductSelector() {
  const {data, error} = useFetch(url)

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

  if (!data) return <Spinner />

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

All going well, you'll 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 being saved to the document.

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

Now we're getting somewhere! The Select menu looks great and contains all the data we pulled from the API, but it doesn't actually do anything.

Now we've got a selectable select menu ... it just doesn't write to the Document

We'll need to patch the document each time the select menu is changed, to save the item id to the document.

To do this, we've brought in a lot of code from the Custom input components docs. I've trimmed the comments out of the below example, so for more details, read the docs.

Our Component is about to get a bit more complicated – this is so Studio UI like readOnly and Presence will still work as expected. So it's super important!

Here's our final Component. To recap, it will:

  1. Take on all the properties of our registered schema, like title and description
  2. Query our provided API endpoint to populate a select menu
  3. On change, save the value of that select menu option to the document
import React from 'react'
import {useFetch} from 'usehooks-ts'
import {Card, Spinner, Text, Select} from '@sanity/ui'
import {FormField} from '@sanity/base/components'
import PatchEvent, {set, unset} from '@sanity/form-builder/PatchEvent'

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

const ProductSelector = React.forwardRef((props, ref) => {
  const {
    type,
    value,
    readOnly,
    placeholder,
    markers,
    presence,
    compareValue,
    onFocus,
    onBlur,
    onChange,
  } = props

  const {data, error} = useFetch(url)

  const handleChange = React.useCallback(
    // useCallback will help with performance
    (event) => {
      const inputValue = event.currentTarget.value
      // if the value exists, set the data, if not, unset the data
      onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()))
    },
    [onChange]
  )

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

  if (!data) return <Spinner />

  return (
    <FormField
      description={type.description}
      title={type.title}
      __unstable_markers={markers}
      __unstable_presence={presence}
      compareValue={compareValue}
    >
      <Card>
        <Select
          ref={ref}
          readOnly={readOnly}
          value={value}
          onFocus={onFocus}
          onBlur={onBlur}
          onChange={handleChange}
        >
          {placeholder && <option disabled>{placeholder}</option>}
          {data.map((item) => (
            <option key={item.id} value={item.id}>
              {item.title} (${item.price})
            </option>
          ))}
        </Select>
      </Card>
    </FormField>
  )
})

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 we can do. Some examples:

  1. Add a Button to "reset" the field so editors can remove the value from the document.
  2. Save needless API requests on page load by having a "Fetch" button to manually populate the Select menu.
  3. 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.
  4. Swap out <Select> for the more powerful Sanity UI <Autocomplete> for smarter searching and visual previews of returned data.

Other guides by author