Building Custom Components in Sanity CMS v3 (Part I)

Carlos Lam

Software Engineer

3 min

Article Banner

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 represented by an array of objects with a visual interface:

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 visually 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 {defineField, defineType} from 'sanity'
2import CustomBracket from '../CustomBracket'
3
4// document schema
5export default {
6  name: 'exampleDoc',
7  type: 'document',
8  title: 'Example Doc',
9  fields: [
10    ...,
11    defineField({
12      name: 'entries',
13      title: 'Bracket Entries',
14      type: 'array',
15      of: [...],
16      // declaration to represent with custom React Component
17      components: {
18        input: CustomBracket
19      }, 
20    })
21  ]
22}

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 minimal example on how to implement a custom input component.

Inside our component, we can get the value of entries by using Studio React Hook useFormValue:

1// custom React component
2import {useFormValue} from 'sanity'
3
4export default function CustomBracket(props) {
5  const {onChange, value = '', id, focusRef, onBlur, onFocus, readOnly} = props
6  
7  const entries = useFormValue(['entries'])
8  
9  return (
10    ... 
11  )
12}

Imagine that we want to be able to generate a sports tournament bracket like in the first image at the beginning of the article. Presumably we would want to generate an empty bracket, and then be able to click on drop-downs to select teams.

To deal with the generation of the bracket, inside our component we have access to an onChange function from props, allowing us to perform patch operations to mutate the document. Combined with the patch events provided by Sanity (set, unset, etc.), we can mutate a simple field of a document. We can generate an empty bracket by mutating the document:

1// custom React component
2import {set, unset} from 'sanity'
3
4export default function CustomBracket(props) {
5  const {onChange, value = '', id, focusRef, onBlur, onFocus, readOnly} = props
6  
7  const entries = useFormValue(['entries'])
8
9  const handleGenerate = useCallback(
10    () => {
11      const emptyBracketDataStructure = "YOUR REPRESENTATION"
12      onChange(set(emptyBracketDataStructure))
13    }, [onChange])
14  
15  return (
16    ... 
17  )
18}

We would also want to have a handler to respond to user inputs in our UI.

1// custom React component
2import {set, unset} from 'sanity'
3
4export default function CustomBracket(props) {
5  const {onChange, value = '', id, focusRef, onBlur, onFocus, readOnly} = props
6  
7  const entries = useFormValue(['entries'])
8
9  const handleGenerate = useCallback(
10    () => {
11      const emptyBracketDataStructure = "YOUR REPRESENTATION"
12      onChange(set(emptyBracketDataStructure))
13    }, [onChange])
14  
15  const handleChange = useCallback(
16    (value, path) => {
17      onChange(value ? set(emptyBracketDataStructure, path) : unset())
18    }, [onChange])
19  
20  return (
21    ... 
22  )
23}

The idea is that value is the input that we have received from our user, while path is the portion of the data structure we wish to mutate. For example, perhaps we detected a change on the very first drop-down input; then path could be [0, ...] to indicate that we want to change the first element in our array (followed by the rest of the path/keys of your data structure). Information regarding path can be found in documentation for similar use cases under Sanity's Studio React Hooks documentation.

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