status
to _status
in preview and projection resultsThe status
field in preview and projection results has been renamed to _status
to prevent collisions with user-defined status
fields and to follow the convention of using underscore prefix for system attributes.
Before:
const {data} = useDocumentPreview({documentId: '123', documentType: 'product'})
console.log(data?.status?.lastEditedPublishedAt)
After:
const {data} = useDocumentPreview({documentId: '123', documentType: 'product'})
console.log(data?._status?.lastEditedPublishedAt)
This change affects:
PreviewValue
interfaceuseManageFavorite
, useNavigateToStudioDocument
, and useRecordDocumentHistoryEvent
now all suspend.Before:
function MyDocumentAction(props: DocumentActionProps) {
const {documentId, documentType, resourceId} = props
const {favorite, unfavorite, isFavorited, isConnected} = useManageFavorite({
documentId,
documentType,
resourceId
})
return (
<Button
disabled={!isConnected}
onClick={() => isFavorited ? unfavorite() : favorite()}
text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
/>
)
}
After:
function FavoriteButton(props: DocumentActionProps) {
const {documentId, documentType, resourceId} = props
const {favorite, unfavorite, isFavorited} = useManageFavorite({
documentId,
documentType,
resourceId
})
return (
<Button
onClick={() => isFavorited ? unfavorite() : favorite()}
text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
/>
)
}
// Wrap the component with Suspense since the hook may suspend
function MyDocumentAction(props: DocumentActionProps) {
return (
<Suspense fallback={<Button text="Loading..." disabled />}>
<FavoriteButton {...props} />
</Suspense>
)
}
The following hooks now also suspend and must be wrapped in <Suspense>
:
useNavigateToStudioDocument
Before:
function NavigateButton({documentHandle}: {documentHandle: DocumentHandle}) {
const {navigateToStudioDocument, isConnected} = useNavigateToStudioDocument(documentHandle)
return (
<Button
disabled={!isConnected}
onClick={navigateToStudioDocument}
text="Navigate to Studio Document"
/>
)
}
After:
function NavigateButton({documentHandle}: {documentHandle: DocumentHandle}) {
const {navigateToStudioDocument} = useNavigateToStudioDocument(documentHandle)
return (
<Button
onClick={navigateToStudioDocument}
text="Navigate to Studio Document"
/>
)
}
// Wrap the component with Suspense since the hook may suspend
function MyDocumentAction({documentHandle}: {documentHandle: DocumentHandle}) {
return (
<Suspense fallback={<Button text="Loading..." disabled />}>
<NavigateButton documentHandle={documentHandle} />
</Suspense>
)
}
useRecordDocumentHistoryEvent
Before:
function RecordEventButton(props: DocumentActionProps) {
const {documentId, documentType, resourceType, resourceId} = props
const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
documentId,
documentType,
resourceType,
resourceId,
})
return (
<Button
disabled={!isConnected}
onClick={() => recordEvent('viewed')}
text="Viewed"
/>
)
}
After:
function RecordEventButton(props: DocumentActionProps) {
const {documentId, documentType, resourceType, resourceId} = props
const {recordEvent} = useRecordDocumentHistoryEvent({
documentId,
documentType,
resourceType,
resourceId,
})
return (
<Button
onClick={() => recordEvent('viewed')}
text="Viewed"
/>
)
}
// Wrap the component with Suspense since the hook may suspend
function MyDocumentAction(props: DocumentActionProps) {
return (
<Suspense fallback={<Button text="Loading..." disabled />}>
<RecordEventButton {...props} />
</Suspense>
)
}
Renamed hooks for better clarity and consistency:
usePreview
→ useDocumentPreview
useProjection
→ useDocumentProjection
Also renamed associated types to match:
UsePreviewOptions
→ useDocumentPreviewOptions
UsePreviewResults
→ useDocumentPreviewResults
UseProjectionOptions
→ useDocumentProjectionOptions
UseProjectionResults
→ useDocumentProjectionResults
useDocument
return structureThe useDocument
hook now returns its data under a data
property for consistency with other hooks in the SDK.
Before:
// Full document
const product = useDocument({documentId: '123', documentType: 'product'})
console.log(product?.title)
// Path selection
const title = useDocument({
documentId: '123',
documentType: 'product',
path: 'title',
})
console.log(title)
After:
// Full document - now returns {data: T | null}
const {data: product} = useDocument({documentId: '123', documentType: 'product'})
console.log(product?.title) // product is possibly null
// Path selection - now returns {data: T | undefined}
const {data: title} = useDocument({
documentId: '123',
documentType: 'product',
path: 'title',
})
console.log(title) // title is possibly undefined
This version introduces significant improvements for TypeScript users by integrating Sanity TypeGen. While Typegen is optional, using it unlocks strong type safety for documents, queries, and projections. These changes also refine hook signatures for better consistency, even for JavaScript users.
See the TypeScript guide for full setup and usage details.
DocumentHandle
or DatasetHandle
.While literal objects still work, using helpers like createDocumentHandle
(imported from @sanity/sdk-react
) is recommended, especially with TypeScript, to ensure literal types are captured correctly for Typegen.
Before:
// === 🛑 BEFORE ===
// Using literal object
const handle = {
documentId: '123',
documentType: 'book',
dataset: 'production',
projectId: 'abc',
}
After:
// === ✅ AFTER ✨ ===
import {createDocumentHandle} from '@sanity/sdk-react'
// Using helper - recommended
const handle = createDocumentHandle({
documentId: '123',
documentType: 'book',
dataset: 'production',
projectId: 'abc',
})
useQuery
Accepts a single options object containing query
(defined with defineQuery
), params
, and optional projectId
, dataset
, etc.
Before:
// 🛑 BEFORE (does not work)
const {data} = useQuery(
'*[_type == $type]', // Raw query string
{type: 'book'}, // Params
{
// Options object (separate)
projectId: 'abc',
dataset: 'production',
perspective: 'published',
},
)
After:
// === ✅ AFTER ✨ ===
import {defineQuery} from 'groq'
const query = defineQuery('*[_type == $type]') // Defined query
const {data} = useQuery({
// Single options object
query: query,
params: {type: 'book'},
projectId: 'abc', // Optional override
dataset: 'production', // Optional override
perspective: 'published',
})
useDocument
Accepts a single options object, spreading the handle and adding path
if needed.
Before:
// === 🛑 BEFORE ===
// Fetching the whole document
const document = useDocument(docHandle)
// Fetching a specific path
const name = useDocument(docHandle, 'name')
After:
// === ✅ AFTER ✨ ===
// Fetching the whole document
const document = useDocument(docHandle)
// const document = useDocument({...docHandle}) // Or spread handle
// Fetching a specific path
const name = useDocument({...docHandle, path: 'name'}) // Spread handle and add path
useEditDocument
Accepts a single options object, spreading the handle and adding path
if needed.
Before:
// === 🛑 BEFORE ===
// Get setter for the whole document
const setDocument = useEditDocument(docHandle)
// Get setter for a specific path
const setName = useEditDocument(docHandle, 'name')
After:
// === ✅ AFTER ✨ ===
// Get setter for the whole document
const setDocument = useEditDocument(docHandle)
// const setDocument = useEditDocument({...docHandle}) // Or spread handle
// Get setter for a specific path
const setName = useEditDocument({...docHandle, path: 'name'}) // Spread handle and add path
useDocuments
/ usePaginatedDocuments
The filter
option can still be used for complex GROQ filters. However, for simple filtering by type, the documentType
option is preferred and aligns better with Typegen scoping.
Before (Simple type filter):
// === 🛑 BEFORE ===
const {data} = useDocuments({
filter: '_type == "author"',
orderings: [{field: 'name', direction: 'asc'}],
})
After (Simple type filter):
// === ✅ AFTER ✨ ===
const {data} = useDocuments({
documentType: 'author', // Use documentType for simple type filtering
orderings: [{field: 'name', direction: 'asc'}],
})
Complex Filter (Remains similar):
Before:
// === 🛑 BEFORE === (Complex filter)
const {data} = usePaginatedDocuments({
filter: '_type == "author" && count(favoriteBooks) > 0',
// ... other options
})
After:
// === ✅ AFTER ✨ === (Complex filter - use filter)
const {data} = usePaginatedDocuments({
documentType: 'author', // Can still specify type
filter: 'count(favoriteBooks) > 0', // Add additional filter logic
// ... other options
})
useDocumentEvent
Accepts a single options object, spreading the handle and adding the onEvent
callback.
Before:
// === 🛑 BEFORE ===
useDocumentEvent(onEventCallback, docHandle)
After:
// === ✅ AFTER ✨ ===
useDocumentEvent({...docHandle, onEvent: onEventCallback})
createDocument
, editDocument
, publishDocument
, etc.) and types (DocumentHandle
, DatasetHandle
, DocumentAction
) now use generic type parameters (<TDocumentType, TDataset, TProjectId>
) for better type safety with Typegen. Usage generally remains the same, but TypeScript users will see improved type checking.applyDocumentActions
similarly uses these generic types and its return type reflects the potentially typed document result (SanityDocumentResult
).By adopting these changes, especially defineQuery
and defineProjection
, you enable the SDK to leverage Typegen for a much safer and more productive development experience, particularly in TypeScript projects.
Removed Authentication Components and Hooks:
<Login />
component - authentication now redirects to sanity.io/login<LoginLayout />
component and its related propsuseLoginUrls
hook - replaced with useLoginUrl
hook that returns a single login URL<AuthBoundary />
now automatically redirects to sanity.io/login when logged out<LoginCallback />
now renders null during the callback processAuthentication Flow Changes:
Renamed hooks:
useInfiniteList
is now useDocuments
usePaginatedList
is now usePaginatedDocuments
usePermissions
is now useDocumentPermissions
useApplyActions
is now useApplyDocumentActions
(and the applyActions
function is now applyDocumentActions
)Re-exported core SDK: The @sanity/sdk
package is now fully re-exported from @sanity/sdk-react
. This means you only need to install and import from @sanity/sdk-react
to access both React-specific hooks/components and core SDK functions/types. You should update your imports accordingly and remove @sanity/sdk
as a direct dependency if it's no longer needed.
Improved component hierarchy with <SanityApp />
, <SDKProvider />
, and <ResourceProvider />
Simplified document references with explicit projectId
+ dataset
fields
Standardized property names across the SDK
Unified hook interfaces with the handle pattern
We've updated our component hierarchy to provide better flexibility and control over resource management:
<SanityApp />
: The recommended top-level component for most applications<SDKProvider />
: An intermediate component with authentication boundaries (for advanced use cases)<ResourceProvider />
: The foundational component for individual resource configurations<SanityApp />
For most applications, particularly dashboard applications, we recommend using the <SanityApp />
component:
// Single project configuration
<SanityApp
config={{projectId: 'abc1235', dataset: 'production'}}
fallback={<>Loading…</>}
>
<App />
</SanityApp>
// Multiple project configuration
<SanityApp
config={[
{projectId: 'abc1235', dataset: 'production'},
{projectId: 'xyz1235', dataset: 'production'},
]}
fallback={<>Loading…</>}
>
<App />
</SanityApp>
The config
prop replaces the previous sanityConfigs
prop and supports both single and multiple configurations. When providing multiple configurations, the first one in the array will be the default instance.
We've introduced a consistent "handle" pattern across the SDK for working with documents and configuration. This replaces the previous resourceId
concept with more explicit fields.
Before:
const doc: DocumentHandle<Author> = {
_type: 'author',
_id: 'db06bc9e-4608-465a-9551-a10cef478037',
resourceId: 'document:ppsg7ml5.test:db06bc9e-4608-465a-9551-a10cef478037',
}
After:
const doc: DocumentHandle<Author> = {
documentType: 'author', // Previously _type
documentId: 'db06bc9e-4608-465a-9551-a10cef478037', // Previously _id
projectId: 'ppsg7ml5', // From resourceId
dataset: 'test', // From resourceId
}
The SDK now uses three main handle types:
// For project-level operations
interface ProjectHandle {
projectId?: string
}
// For dataset-level operations
interface DatasetHandle extends ProjectHandle {
dataset?: string
}
// For document operations
interface DocumentHandle extends DatasetHandle {
documentId: string
documentType: string
}
Various hooks and associated types have been renamed for clarity. Their signatures remain the same, aside from the use of document handles, which is covered in the next section.
useInfiniteList
is now useDocuments
InfiniteListOptions
is now DocumentsOptions
InfiniteList
is now DocumentsResponse
usePaginatedList
is now usePaginatedDocuments
PaginatedListOptions
is now PaginatedDocumentsOptions
PaginatedList
is now PaginatedDocumentsResponse
useApplyActions
is now useApplyDocumentActions
applyActions
is now applyDocumentActions
ApplyActionsOptions
is now ApplyDocumentActionsOptions
usePermissions
is now useDocumentPermissions
PermissionsResult
is now DocumentPermissionsResult
Many hooks have been updated to use the handle pattern consistently.
Before:
function Preview({document}: {document: DocumentHandle}) {
const {data} = useProjection({document, projection: '{title}'})
const {data: preview} = usePreview({document, ref: someRef})
return // ...
}
After:
interface PreviewProps extends DocumentHandle {
showExtra?: boolean
}
function Preview({showExtra, ...docHandle}: PreviewProps) {
const ref = useRef<HTMLElement>(null)
const {data} = useProjection({...docHandle, ref, projection: '{title}'})
const {data: preview} = usePreview({...docHandle, ref})
return // ...
}
All query-based hooks now accept DatasetHandle
for configuration:
// useQuery with optional project/dataset override
const {data} = useQuery('*[_type == $type][0...10]', {
params: {type: 'author'},
projectId: 'abc12345', // Optional - inherits from ResourceProvider
dataset: 'production', // Optional - inherits from ResourceProvider
})
// List hooks with configuration
const {data: documents} = useDocuments({
filter: '_type == "product"',
projectId: 'xyz12345', // Optional
dataset: 'staging', // Optional
batchSize: 20,
})
// Returned documents include full context
documents.map((docHandle) => (
<DocumentPreview
key={docHandle.documentId}
{...docHandle} // Includes projectId, dataset, etc.
/>
))
Project and dataset hooks now use the handle pattern:
// Before
const project = useProject('abc12345')
// After
const project = useProject({projectId: 'abc12345'})
const datasets = useDatasets({projectId: 'abc12345'})
🔄 Coming Soon: We're continuing to refine our APIs. Future releases will include:
- Further unification of hook signatures
- More consistent parameter naming
- Additional handle pattern improvements
- Enhanced TypeScript types and validations
Authentication Changes:
<Login />
, <LoginLayout />
, and useLoginUrls
<AuthBoundary />
and <LoginCallback />
behavior changesComponent Changes:
<SanityApp />
now uses config
instead of sanityConfigs
<SDKProvider />
now uses config
prop for multiple configurations<ResourceProvider />
provides granular control for single configuration<SanityProvider />
removedHook Renames:
useInfiniteList
is now useDocuments
usePaginatedList
is now usePaginatedDocuments
usePermissions
is now useDocumentPermissions
useApplyActions
is now useApplyDocumentActions
(and the applyActions
function is now applyDocumentActions
)@sanity/sdk
Re-exported: All exports from @sanity/sdk
are now available directly from @sanity/sdk-react
.
Property Renames:
_type
→ documentType
_id
→ documentId
results
→ data
(in hook returns)resourceId
conceptInterface Updates:
DocumentHandle