A new era for content applications with Sanity App SDK

Written by Simeon Griggs

Over the last few years, I've shipped over a dozen plugins and written thousands of words of tutorials and lessons on how to customize Sanity Studio.

The Sanity App SDK is the toolkit I have always wanted.

PortableText [components.type] is missing "protip"
Play

What was always possible is now far simpler with the Sanity App SDK. Until now, building your own content experiences meant building them inside Sanity Studio and having to recreate the Studio's UX.

The Sanity App SDK distills the behaviors authors expect—live updating document lists, automatically creating drafts, multiplayer editing—and into reusable hooks for developers to build novel content applications rapidly.

Table view of events in an editable CMS

With this UI baked in, you're free from technical overhead to build impressive content applications that solve for content creators' needs for immediate business impact. Build fit-for-purpose applications instead of trying to mutate a cookie-cutter CMS into something it was never meant to be.

Google map showing pins for event locations

The App SDK welcomes developers to an ambitious future that is less about prying apart Sanity Studio code and instead about building entirely new content experiences, faster than ever.

Calendar with event titles rendered

Content operations are jobs to be done

With Sanity Dashboard (the new "home screen" of the Content Operating System), the content apps you build can be deployed independently to operate across projects and datasets.

Our team has over eight years of experience building real-time content editing experiences for content creators. The App SDK is our way of distilling all we've learned into a distributed library for developers and the content creators they empower.

It's time to build, not just configure

The primary way to work with Sanity content has always been the Sanity Studio. Being truly "headless," it is built on the same APIs developers could use to make their applications perform queries and mutations using either the Sanity Client or the HTTP API.

However, Sanity Studio has a few opinionated user experience implementation details that authors come to expect, like the way editing a published document immediately creates a new draft or how a rendered list of documents stays updated in real-time.

It could be disorienting if you needed to build your own content review or editing experience, and what you built didn't work the same way.

So while developers have always loved Sanity Studio for its configurability and customizability, building entirely new experiences has been more difficult than we were ever happy with.

That changes now.

Introducing App SDK

Headless CMSes have only ever promised multi-channel to create multiple front-ends that query and display content.

Sanity App SDK fulfills the content operating system promise by allowing the creation of multiple content editing applications.

A toolkit for initializing apps

The Sanity CLI now contains a command to bootstrap a new Sanity app, which can be deployed to the Dashboard.

npx sanity@latest init --template app-sanity-ui

This template comes preconfigured with Vite, TypeScript, and Sanity UI—what you do next is up to you.

Headless, by design

App SDK is "headless," meaning it does not come with visual components or styles. All of its functionality is centered around querying and modifying content data. It's up to you how to style the user interface.

This template includes Sanity UI—but your custom apps can be styled with Tailwind, shadcn/ui, Bootstrap, jQuery UI, or just raw CSS if you're freaky.

For consistency and familiarity we recommend using the same component library as all Sanity apps—Sanity UI—but you're welcome to ignore our recommendation.

React context and hooks

The React package makes available a SanityApp provider to wrap your application to perform queries and mutations. This context also contains internal logic for optimistic edits and caching so that all queries and mutations are self-updating and super fast.

Many hooks are exported by the React package, read about them all in the documentation. Highlights include:

TypeScript support

Sanity App SDK pairs nicely with Sanity TypeGen (currently in Beta) for type safety across all your schema types and query responses.

See the documentation on how to setup TypeGen with App SDK apps.

Code examples

Every App built with the SDK starts with the SanityApp wrapper, configured with settings for multiple projects.

You read that correctly, multiple projects. Unlike the Studio which could only ever target a single project and dataset. App SDK apps are designed to span across multiple configurations.

PortableText [components.type] is missing "infoBox"

SanityApp context

// App.tsx

import {type SanityConfig} from '@sanity/sdk-react'
import {SanityApp} from '@sanity/sdk-react'
import {ExampleComponent} from './ExampleComponent'
import './App.css'

export function App() {
  // apps can access many different projects or other sources of data
  const sanityConfigs: SanityConfig[] = [
    {
      projectId: 'project-id',
      dataset: 'dataset-name',
    }
  ]

  return (
    <div className="app-container">
      <SanityApp config={sanityConfigs} fallback={<div>Loading...</div>}>
        {/* add your own components here! */}
        <ExampleComponent />
      </SanityApp>
    </div>
  )
}

export default App

The SanityApp context will handle deduplicating, memoizing and optimizing every fetch for content, so there's no need to do your own caching or memoization.

useDocuments

You may be used to querying all required content in a single, huge GROQ query passed into client.fetch(). But most SDK apps begin with a fetch for documents of a certain _type, and resolve each matched document individually.

// DocumentsList.tsx

import {useDocuments} from '@sanity/sdk-react'
import {DocumentTitle} from './DocumentTitle'

export function DocumentsList() {
  const {data} = useDocuments({filter: '_type == "event'})

  return (
    <ul>
      {data.map((doc) => (
        <li key={doc.documentId}>
          <DocumentTitle {...doc} />
        </li>
      ))}
    </ul>
  )
}

Rendering a live-updating list of documents can be done with the useDocuments hook. By default the hook will only return up to 25 documents, and more can be fetched via the loadMore function returned by the hook.

For improved performance, the data returned by this hook is deliberately kept small. An array of "document handles." Each document's _id and _type fields, represented as documentId and documentType.

useDocument

// DocumentTitle.tsx

import {type DocumentHandle} from '@sanity/sdk-react'
import {useDocument} from '@sanity/sdk-react'

export function DocumentTitle(props: DocumentHandle) {
  const {data} = useDocument(props)

  return <h2>{data.title}</h2>
}

For high-performance, real-time updates the useDocument hook retrieves the content of an entire document, or specific values using a path. This hook can be intensive, so it's best used lightly. Fortunately there are hooks for memory friendly data fetching and rendering.

useDocumentProjection

// DocumentDetail.tsx

import {type DocumentHandle} from '@sanity/sdk-react'
import {useDocumentProjection} from '@sanity/sdk-react'

export function DocumentDetail(props: DocumentHandle) {
  const {data} = useDocumentProjection({
    ...props, 
    projection: `{ title, categories[]->{title} }`
  })

  return (
    <div>
      <h2>{data.title}</h2>
      <p>{data.categories.map((category) => category.title).join(', ')}</p>
    </div>
  )
}

This hook allows you to select individual attributes from a document—or transform them since GROQ is so flexible—as well as resolve references.

useEditDocument

Now for some real magic.

// EditDocumentTitle.tsx

import {type DocumentHandle} from '@sanity/sdk-react'
import {
  useDocument,
  useEditDocument,
} from '@sanity/sdk-react'

export function EditDocumentTitle(props: DocumentHandle) {
  const handleAndPath = {
    ...props,
    path: 'title',
  }
  const {data: title} = useDocument(handleAndPath)
  const editTitle = useEditDocument(handleAndPath)

  return (
    <input
      value={title}
      onChange={(e) => editTitle(e.target.value)}
    />
  )
}

You can edit any value of a document with the useEditDocument hook. In this example simply editing a string type field value with an input element.

This hook handles the logic of creating a new draft if the current document is already published. It also updates the in-memory context so your UI has local-first, optimistic updates while they are synced to the Content Lake in the background.

useApplyDocumentActions

// PublishDocument.tsx

import {type DocumentHandle, publishDocument} from '@sanity/sdk-react'
import {useApplyDocumentActions} from '@sanity/sdk-react'

export function PublishDocument(props: DocumentHandle) {
  const applyActions = useApplyDocumentActions()
  const publishAction = applyActions([
    publishDocument(props),
  ])

  return (
    <button onClick={() => publishAction}>Publish</button>
  )
} 

To take actions on a single, or multiple, documents the useApplyActions provides a function which accepts an array of action functions. Use this to publish or discard drafts, or create granular edits across multiple documents in one transaction.

More hooks are exported by the React package, read about them all in the documentation.

Rocket-boosters for your content apps

The App SDK has been designed and built in close consultation with many of our largest customers who have created their own bespoke Sanity content apps or deep Sanity Studio customizations.

We can't wait to see what you'll build. If you'd love to show it off, tag us on social media or join our community on Discord and post up in the Showcase channel.