Sanity Library Reference Docs
    Preparing search index...

    Module @sanity/sdk-react

    Sanity App SDK (React)

    React hooks for creating Sanity applications. Live by default, optimistic updates, multi-project support.


    npx sanity@latest init --template app-quickstart
    cd your-app
    npm run dev

    Opens at https://www.sanity.io/welcome?dev=http%3A%2F%2Flocalhost%3A3333, proxied through Sanity Dashboard for auth.

    Key files:

    • sanity.cli.ts — configuration options used by the CLI — application metadata, deployment config, etc
    • src/App.tsx — Root with <SanityApp> provider and project configuration(s)
    • src/ExampleComponent.tsx — Your starting point
    import {SanityApp, type SanityConfig} from '@sanity/sdk-react'

    const config: SanityConfig[] = [
    {projectId: 'abc123', dataset: 'production'},
    {projectId: 'def456', dataset: 'production'}, // multi-project support
    ]

    export function App() {
    return (
    <SanityApp config={config} fallback={<div>Loading...</div>}>
    <YourApp />
    </SanityApp>
    )
    }

    Auth is automatic — Dashboard injects an auth token via iframe. No custom login flow is needed for your application.


    Document handles are a core concept for apps built with the App SDK. Document handles are minimal pointers to documents. They consist of the following properties:

    type DocumentHandle = {
    documentId: string
    documentType: string
    projectId?: string // optional if using the default projectId or inside a ResourceProvider
    dataset?: string // optional if using the default dataset or inside a ResourceProvider
    }

    Best practice: Fetch document handles first → pass them to child components → fetch individual document content from child components.


    // Get a collection of document handles (structured for infinite scrolling)
    const {data, hasMore, loadMore, isPending, count} = useDocuments({
    documentType: 'article',
    batchSize: 20,
    orderings: [{field: '_updatedAt', direction: 'desc'}],
    filter: 'status == $status', // GROQ filter
    params: {status: 'published'}, // Parameters used for the GROQ filter
    })

    // Get a collection of document handles (structured for paginated lists)
    const {data, currentPage, totalPages, nextPage, previousPage} = usePaginatedDocuments({
    documentType: 'article',
    pageSize: 10,
    })

    // Get content from a single document (live content, optimistic updates when used with useEditDocument)
    const {data: doc} = useDocument(handle)
    const {data: title} = useDocument({...handle, path: 'title'})

    // Get a projection for an individual document (live content, no optimistic updates)
    const {data} = useDocumentProjection({
    ...handle,
    projection: `{ title, "author": author->name, "imageUrl": image.asset->url }`,
    })

    // Use GROQ directly
    const {data} = useQuery({
    query: `*[_type == "article" && featured == true][0...5]{ title, slug }`,
    })
    // Edit field (emits optimistic updates to useEditDocument listeners, creates a draft automatically)
    const editTitle = useEditDocument({...handle, path: 'title'})
    editTitle('New Title') // fires on every keystroke, debounced internally

    // Edit a nested path in a document
    const editAuthorName = useEditDocument({...handle, path: 'author.name'})

    // Document actions
    import {
    useApplyDocumentActions,
    createDocumentHandle,
    publishDocument,
    unpublishDocument,
    deleteDocument,
    createDocument,
    discardDraft,
    } from '@sanity/sdk-react'

    const apply = useApplyDocumentActions()

    // Single action
    await apply(publishDocument(handle))

    // Batch actions
    await apply([publishDocument(handle1), publishDocument(handle2), deleteDocument(handle3)])

    // Create new document with an optional initial content
    const newHandle = createDocumentHandle({
    documentId: crypto.randomUUID(),
    documentType: 'article',
    })
    await apply(createDocument(newHandle, {title: 'Untitled', status: 'draft'}))
    // Subscribe to document events
    useDocumentEvent({
    ...handle,
    onEvent: (event) => {
    // event.type: 'documentEdited' | 'documentPublished' | 'documentDeleted' | ...
    console.log(event.type, event.documentId)
    },
    })

    // Check permissions
    const {data: canEdit} = useDocumentPermissions({
    ...handle,
    permission: 'update',
    })
    const {data: canPublish} = useDocumentPermissions({
    ...handle,
    permission: 'publish',
    })

    The useApplyDocumentActions hook is used to perform document lifecycle operations. Actions are created using helper functions and applied through the apply function.

    Function Description
    createDocument Create a new document
    publishDocument Publish a draft (copy draft → published)
    unpublishDocument Unpublish (delete published, keep draft)
    deleteDocument Delete document entirely (draft and published)
    discardDraft Discard draft changes, revert to published

    To create a document, you must:

    1. Generate your own document ID (using crypto.randomUUID())
    2. Create a document handle with createDocumentHandle
    3. Apply the createDocument action using the document handle, along with optional initial content
    import {useApplyDocumentActions, createDocumentHandle, createDocument} from '@sanity/sdk-react'

    function CreateArticleButton() {
    const apply = useApplyDocumentActions()

    const handleCreateArticle = () => {
    const newId = crypto.randomUUID()
    const handle = createDocumentHandle({
    documentId: newId,
    documentType: 'article',
    })

    apply(
    createDocument(handle, {
    title: 'New Article',
    status: 'draft',
    author: {_type: 'reference', _ref: 'author-123'},
    }),
    )

    // Navigate to the new document
    navigate(`/articles/${newId}`)
    }

    return <button onClick={handleCreateArticle}>Create Article</button>
    }
    import {useApplyDocumentActions, publishDocument, useDocument} from '@sanity/sdk-react'

    function PublishButton({handle}: {handle: DocumentHandle}) {
    const apply = useApplyDocumentActions()
    const {data: doc} = useDocument(handle)

    // Check if document has unpublished changes (is a draft)
    const isDraft = doc?._id?.startsWith('drafts.')

    return (
    <button disabled={!isDraft} onClick={() => apply(publishDocument(handle))}>
    Publish
    </button>
    )
    }
    import {useApplyDocumentActions, deleteDocument} from '@sanity/sdk-react'

    function DeleteButton({handle}: {handle: DocumentHandle}) {
    const apply = useApplyDocumentActions()

    const handleDelete = () => {
    if (confirm('Are you sure?')) {
    apply(deleteDocument(handle))
    }
    }

    return <button onClick={handleDelete}>Delete</button>
    }

    Apply multiple actions as a single transaction:

    const apply = useApplyDocumentActions()

    // Create and immediately publish
    const newHandle = createDocumentHandle({
    documentId: crypto.randomUUID(),
    documentType: 'article',
    })

    apply([createDocument(newHandle, {title: 'Breaking News'}), publishDocument(newHandle)])

    // Publish multiple documents at once
    apply([publishDocument(handle1), publishDocument(handle2), publishDocument(handle3)])

    All hooks that get or write data use React Suspense. Wrap all your components that fetch data with a Suspense boundary to avoid unnecessary re-renders:

    function App() {
    return (
    <Suspense fallback={<Skeleton />}>
    <ArticleList />
    </Suspense>
    )
    }

    function ArticleList() {
    const {data: articles} = useDocuments({documentType: 'article'})

    return (
    <ul>
    {articles.map((handle) => (
    {/* Wrap each list item in its own Suspense boundary to prevent full list re-renders when one item updates */}
    <Suspense key={handle.documentId} fallback={<li>Loading...</li>}>
    <ArticleItem handle={handle} />
    </Suspense>
    ))}
    </ul>
    )
    }

    Sanity has two document states:

    • Published: _id: "abc123" — live, public
    • Draft: _id: "drafts.abc123" — working copy

    The SDK handles updating the document state automatically:

    • useDocument() returns draft if exists, else published
    • useEditDocument() creates draft on first edit (automatic)
    • publishDocument() copies draft → published, deletes draft
    • discardDraft() deletes draft, reverts to published

    • Document changes from other users appear instantly
    • No polling, uses Sanity's listener API
    • Optimistic updates for local edits appear before the server confirms the updates

    Any mutation to a subscribed document (even fields you don't display) will trigger a re-render. Use useDocumentProjection() for read-only displays to minimize re-renders.


    const handle: DocumentHandle = {
    documentId: 'xyz',
    documentType: 'product',
    projectId: 'project-a',
    dataset: 'production',
    }
    // App.tsx
    import {ResourceProvider} from '@sanity/sdk-react'

    import {ProductCard} from './ProductCard'

    export function WrappedProductCard() {
    return (
    <ResourceProvider projectId="project-a" dataset="production">
    <ProductCard productId="xyz" />
    </ResourceProvider>
    )
    }

    // ProductCard.tsx
    import {useProjectId, useDataset} from '@sanity/sdk-react'

    function ProductCard({productId}: {productId: string}) {
    const projectId = useProjectId() // "project-a" from nearest configured ResourceProvider
    const dataset = useDataset() // "production" from nearest configured ResourceProvider
    // ...
    }

    # Generate types from your schema
    npx sanity typegen generate
    import type {Article} from './sanity.types'

    const {data} = useDocument<Article>(handle)
    // data is typed as Article

    npx sanity deploy
    

    Add the resulting app ID to the deployment section of your sanity.config.ts file: {deployment: { appId: "appbc1234", ... } }.

    App appears in Sanity Dashboard alongside Studios. Requires sanity.sdk.applications.deploy permission.


    SDK is headless. Common choices:

    # Sanity UI (matches Studio aesthetic)
    npm install @sanity/ui @sanity/icons styled-components

    # Tailwind
    npm install tailwindcss @tailwindcss/vite

    Tailwind requires a few extra steps since the App SDK uses Vite internally.

    1. Install dependencies:
    npm install tailwindcss @tailwindcss/vite
    
    1. Configure the Vite plugin in sanity.cli.ts:
    import {defineCliConfig} from 'sanity/cli'

    export default defineCliConfig({
    app: {
    organizationId: 'your-org-id',
    entry: './src/App.tsx',
    },
    vite: async (viteConfig) => {
    const {default: tailwindcss} = await import('@tailwindcss/vite')
    return {
    ...viteConfig,
    plugins: [...viteConfig.plugins, tailwindcss()],
    }
    },
    })
    1. Import Tailwind in your CSS (e.g., src/App.css):
    @import 'tailwindcss';
    
    1. Import the CSS in your app:
    // src/App.tsx
    import './App.css'

    Now you can use Tailwind classes in your components.

    Use @portabletext/plugin-sdk-value to connect a Portable Text Editor with a Sanity document field. It provides two-way sync, real-time collaboration, and optimistic updates.

    1. Install dependencies:
    npm install @portabletext/editor @portabletext/plugin-sdk-value
    
    1. Use in a component:
    import {defineSchema, EditorProvider, PortableTextEditable} from '@portabletext/editor'
    import {SDKValuePlugin} from '@portabletext/plugin-sdk-value'

    function MyEditor({documentId}: {documentId: string}) {
    return (
    <EditorProvider initialConfig={{schemaDefinition: defineSchema({})}}>
    <PortableTextEditable />
    <SDKValuePlugin documentId={documentId} documentType="article" path="content" />
    </EditorProvider>
    )
    }
    Prop Type Description
    documentId string The document ID
    documentType string The document type
    path string JSONMatch path to the Portable Text field
    dataset string (optional) Dataset name if different from default
    projectId string (optional) Project ID if different from default

    The plugin handles:

    • Two-way sync between editor and document
    • Real-time updates from other users
    • Optimistic updates for smooth UX

    function EditableTitle({handle}: {handle: DocumentHandle}) {
    const {data: title} = useDocument({...handle, path: 'title'})
    const editTitle = useEditDocument({...handle, path: 'title'})

    return <input value={title ?? ''} onChange={(e) => editTitle(e.target.value)} />
    }
    function PublishButton({handle}: {handle: DocumentHandle}) {
    const {data: canPublish} = useDocumentPermissions({
    ...handle,
    permission: 'publish',
    })
    const apply = useApplyDocumentActions()

    if (!canPublish) return null

    return <button onClick={() => apply(publishDocument(handle))}>Publish</button>
    }
    function DocStatus({handle}: {handle: DocumentHandle}) {
    const {data: published} = useDocumentProjection({
    documentId: handle.documentId, // without drafts. prefix
    documentType: handle.documentType,
    projection: '{ _updatedAt }',
    })

    const {data: draft} = useDocumentProjection({
    documentId: `drafts.${handle.documentId}`,
    documentType: handle.documentType,
    projection: '{ _updatedAt }',
    })

    if (draft && published) return <span>Modified</span>
    if (draft) return <span>Draft</span>
    if (published) return <span>Published</span>
    return <span>New</span>
    }

    Task Hook/Function
    List documents useDocuments, usePaginatedDocuments
    Read document useDocument, useDocumentProjection
    Edit field useEditDocument
    Publish/Delete/Create useApplyDocumentActions + action creators
    GROQ query useQuery
    Check permissions useDocumentPermissions
    Listen to changes useDocumentEvent

    MIT © Sanity.io

    Documents

    Migration guide
    Typescript with TypeGen (experimental)

    Modules

    exports