Building Custom Components in Sanity CMS (Part I)

Carlos Lam

Software Engineer

3 min

Article Banner

EDIT: This article was written as of Sanity Studio v2. To see the equivalent for Sanity Studio v3, please see Building Custom Components in Sanity CMS v3 (Part I)

This article assumes prior experience to using Sanity, and a basic working knowledge on the usage of Sanity Content Lake and Studio.

For web applications where a CMS is a requirement, Sanity's Content Lake and Studio offers a quick way to incorporate a customizable flow for non-technical users to update content.

While Studio has out-of-the-box components to represent simple data structures such as those that make up a simple blog, more complex data types may require more specific methods of representation. For instance, a sports tournament bracket represented by an object nested several-levels deep - with team and seeding data - could be visually represented as:

Sanity Studio tournament bracket example

Sanity Studio tournament bracket example

We can do this by leveraging Studio's custom input components, which essentially enables us to represent our data structure through React components. All that is required is to implement the React component and to declare it inside the field of the document.

1import CustomBracket from '../CustomBracket'
3// document schema
4export default {
5  name: 'exampleDoc',
6  type: 'document',
7  title: 'Example Doc',
8  fields: [
9    ...,
10    {
11      // declaration to represent with custom React Component
12      inputComponent: CustomBracket, 
13      name: 'entries',
14      title: 'Bracket Entries',
15      type: 'array',
16      of: [...]
17    }
18  ]

The above is the aforementioned bracket, named `entries`, declared inside an example document schema. `CustomBracket` is a React component, and we can look towards Sanity's documentation on how to start implementing a custom input component.

Within the component, we have access to the parent document, enabling us to retrieve the document `_id`. Using this, we can perform operations to mutate the document:

1// custom React component
2import { useDocumentOperation } from '@sanity/react-hooks'
3import { withDocument } from 'part:@sanity/form-builder'
5const CustomBracket = React.forwardRef((props) => {
6  const { _id, type, entries, } = props.document
8  const id = _id ? _id.replace('drafts.', '') : 'someRandomPlaceholder'
10  const { patch } = useDocumentOperation(id, type)
12  return (
13    ... 
14  )
17export default withDocument(CustomBracket)

Note the ternary operation to check for the existence of `_id`; the `useDocumentOperation` hook requires an `_id` be supplied, so a placeholder is given to avoid edge case errors.

Another thing to note: as of the time that this article was written, mutations to a nested property of a field requires replacing the whole field (e.g. in the earlier schema, `entries` is to be replaced even if only a nested property of `entries` is changed). Therefore, the data structure parsing and mutation is left for the developer to implement:

1// custom React component
2import { useDocumentOperation } from '@sanity/react-hooks'
3import { withDocument } from 'part:@sanity/form-builder'
5const CustomBracket = React.forwardRef((props) => {
6  const { _id, type, entries, } = props.document
7  const id = _id ? _id.replace('drafts.', '') : 'someRandomPlaceholder'
8  const { patch } = useDocumentOperation(id, type)
10  const entriesMutation = (entries) => {
11    // implementation to parse and mutate data in `entries` field
12    return newEntries
13  }
15  // applying the changes
16  const handlePatch = (newEntries) => {
17    patch.execute([
18      {
19        set: {
20          entries: newEntries
21        }
22      }
23    ])
24  }
26  const handleChange = (entries) => {
27    const newEntries = entriesMutation(entries)
28    handlePatch(newEntries)
29  }
31  return (
32    ... 
33  )
36export default withDocument(CustomBracket)

The above shows a full high-level overview of the custom component's flow; a function to handle changes in the `entries` field is used whenever a change to the data needs to be made. The function makes a copy of the data, parses and mutates it, and executes the patch to update the document.

The resulting flow that a user in Studio is left with matches the out-of-the-box flow for non-custom components. Such usage of custom components with more complex data structures makes Sanity extremely versatile. For one, this allows developers to incorporate schemas with more complex data types while maintaining the ability to allow users (such as content creators) to add and update data. There are endless possibilities, and we will explore more ways to leverage this customizability in the upcoming parts of this article series.