@sanity/sdk-react 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +164 -19
  2. package/dist/index.d.ts +571 -26
  3. package/dist/index.js +149 -78
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -7
  6. package/src/_exports/sdk-react.ts +2 -0
  7. package/src/components/SDKProvider.tsx +8 -3
  8. package/src/components/SanityApp.test.tsx +72 -2
  9. package/src/components/SanityApp.tsx +53 -10
  10. package/src/components/auth/AuthBoundary.tsx +5 -5
  11. package/src/context/ComlinkTokenRefresh.test.tsx +2 -2
  12. package/src/context/ComlinkTokenRefresh.tsx +3 -2
  13. package/src/context/SDKStudioContext.test.tsx +126 -0
  14. package/src/context/SDKStudioContext.ts +65 -0
  15. package/src/context/SourcesContext.tsx +7 -0
  16. package/src/context/renderSanityApp.test.tsx +355 -0
  17. package/src/context/renderSanityApp.tsx +48 -0
  18. package/src/hooks/agent/agentActions.ts +436 -21
  19. package/src/hooks/dashboard/useDispatchIntent.test.ts +26 -20
  20. package/src/hooks/dashboard/useDispatchIntent.ts +10 -11
  21. package/src/hooks/dashboard/utils/{getResourceIdFromDocumentHandle.test.ts → useResourceIdFromDocumentHandle.test.ts} +33 -60
  22. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +46 -0
  23. package/src/hooks/document/useEditDocument.ts +3 -0
  24. package/src/hooks/documents/useDocuments.ts +3 -2
  25. package/src/hooks/helpers/useNormalizedSourceOptions.ts +85 -0
  26. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +1 -0
  27. package/src/hooks/projection/useDocumentProjection.ts +15 -4
  28. package/src/hooks/query/useQuery.ts +30 -11
  29. package/src/hooks/dashboard/types.ts +0 -12
  30. package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.ts +0 -53
@@ -1,10 +1,10 @@
1
1
  import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
- import {type FrameMessage} from '@sanity/sdk'
2
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
3
3
  import {useCallback} from 'react'
4
4
 
5
5
  import {useWindowConnection} from '../comlink/useWindowConnection'
6
- import {type DocumentHandleWithSource} from './types'
7
- import {getResourceIdFromDocumentHandle} from './utils/getResourceIdFromDocumentHandle'
6
+ import {type WithSourceNameSupport} from '../helpers/useNormalizedSourceOptions'
7
+ import {useResourceIdFromDocumentHandle} from './utils/useResourceIdFromDocumentHandle'
8
8
 
9
9
  /**
10
10
  * Message type for sending intents to the dashboard
@@ -42,7 +42,7 @@ interface DispatchIntent {
42
42
  interface UseDispatchIntentParams {
43
43
  action?: 'edit'
44
44
  intentId?: string
45
- documentHandle: DocumentHandleWithSource
45
+ documentHandle: WithSourceNameSupport<DocumentHandle>
46
46
  parameters?: Record<string, unknown>
47
47
  }
48
48
 
@@ -104,14 +104,14 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
104
104
  connectTo: SDK_CHANNEL_NAME,
105
105
  })
106
106
 
107
+ const resource = useResourceIdFromDocumentHandle(documentHandle)
108
+
107
109
  const dispatchIntent = useCallback(() => {
108
110
  try {
109
111
  if (!action && !intentId) {
110
112
  throw new Error('useDispatchIntent: Either `action` or `intentId` must be provided.')
111
113
  }
112
114
 
113
- const {projectId, dataset, source} = documentHandle
114
-
115
115
  if (action && intentId) {
116
116
  // eslint-disable-next-line no-console -- warn if both action and intentId are provided
117
117
  console.warn(
@@ -119,14 +119,13 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
119
119
  )
120
120
  }
121
121
 
122
- if (!source && (!projectId || !dataset)) {
122
+ // Validate that we have a resource ID (which is computed from source/sourceName or projectId+dataset)
123
+ if (!resource.id) {
123
124
  throw new Error(
124
- 'useDispatchIntent: Either `source` or both `projectId` and `dataset` must be provided in documentHandle.',
125
+ 'useDispatchIntent: Unable to determine resource. Either `source`, `sourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
125
126
  )
126
127
  }
127
128
 
128
- const resource = getResourceIdFromDocumentHandle(documentHandle)
129
-
130
129
  const message: IntentMessage = {
131
130
  type: 'dashboard/v1/events/intents/dispatch-intent',
132
131
  data: {
@@ -150,7 +149,7 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
150
149
  console.error('Failed to dispatch intent:', error)
151
150
  throw error
152
151
  }
153
- }, [action, intentId, documentHandle, parameters, sendMessage])
152
+ }, [action, intentId, documentHandle, parameters, sendMessage, resource.id, resource.type])
154
153
 
155
154
  return {
156
155
  dispatchIntent,
@@ -1,28 +1,21 @@
1
- import {
2
- canvasSource,
3
- datasetSource,
4
- type DocumentHandle,
5
- type DocumentSource,
6
- mediaLibrarySource,
7
- } from '@sanity/sdk'
8
1
  import {describe, expect, it} from 'vitest'
9
2
 
10
- import {type DocumentHandleWithSource} from '../types'
11
- import {getResourceIdFromDocumentHandle} from './getResourceIdFromDocumentHandle'
3
+ import {renderHook} from '../../../../test/test-utils'
4
+ import {useResourceIdFromDocumentHandle} from './useResourceIdFromDocumentHandle'
12
5
 
13
6
  describe('getResourceIdFromDocumentHandle', () => {
14
7
  describe('with traditional DocumentHandle (projectId/dataset)', () => {
15
8
  it('should return resource ID from projectId and dataset', () => {
16
- const documentHandle: DocumentHandle = {
9
+ const documentHandle = {
17
10
  documentId: 'test-document-id',
18
11
  documentType: 'test-document-type',
19
12
  projectId: 'test-project-id',
20
13
  dataset: 'test-dataset',
21
14
  }
22
15
 
23
- const result = getResourceIdFromDocumentHandle(documentHandle)
16
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
24
17
 
25
- expect(result).toEqual({
18
+ expect(result.current).toEqual({
26
19
  id: 'test-project-id.test-dataset',
27
20
  type: undefined,
28
21
  })
@@ -31,33 +24,33 @@ describe('getResourceIdFromDocumentHandle', () => {
31
24
 
32
25
  describe('with DocumentHandleWithSource - media library', () => {
33
26
  it('should return media library ID and resourceType when media library source is provided', () => {
34
- const documentHandle: DocumentHandleWithSource = {
27
+ const documentHandle = {
35
28
  documentId: 'test-asset-id',
36
29
  documentType: 'sanity.asset',
37
- source: mediaLibrarySource('mlPGY7BEqt52'),
38
- }
30
+ sourceName: 'media-library',
31
+ } as const
39
32
 
40
- const result = getResourceIdFromDocumentHandle(documentHandle)
33
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
41
34
 
42
- expect(result).toEqual({
43
- id: 'mlPGY7BEqt52',
35
+ expect(result.current).toEqual({
36
+ id: 'media-library-id',
44
37
  type: 'media-library',
45
38
  })
46
39
  })
47
40
 
48
41
  it('should prioritize source over projectId/dataset when both are provided', () => {
49
- const documentHandle: DocumentHandleWithSource = {
42
+ const documentHandle = {
50
43
  documentId: 'test-asset-id',
51
44
  documentType: 'sanity.asset',
52
45
  projectId: 'test-project-id',
53
46
  dataset: 'test-dataset',
54
- source: mediaLibrarySource('mlPGY7BEqt52'),
47
+ sourceName: 'media-library',
55
48
  }
56
49
 
57
- const result = getResourceIdFromDocumentHandle(documentHandle)
50
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
58
51
 
59
- expect(result).toEqual({
60
- id: 'mlPGY7BEqt52',
52
+ expect(result.current).toEqual({
53
+ id: 'media-library-id',
61
54
  type: 'media-library',
62
55
  })
63
56
  })
@@ -65,16 +58,16 @@ describe('getResourceIdFromDocumentHandle', () => {
65
58
 
66
59
  describe('with DocumentHandleWithSource - canvas', () => {
67
60
  it('should return canvas ID and resourceType when canvas source is provided', () => {
68
- const documentHandle: DocumentHandleWithSource = {
61
+ const documentHandle = {
69
62
  documentId: 'test-canvas-document-id',
70
63
  documentType: 'sanity.canvas.document',
71
- source: canvasSource('canvas123'),
64
+ sourceName: 'canvas',
72
65
  }
73
66
 
74
- const result = getResourceIdFromDocumentHandle(documentHandle)
67
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
75
68
 
76
- expect(result).toEqual({
77
- id: 'canvas123',
69
+ expect(result.current).toEqual({
70
+ id: 'canvas-id',
78
71
  type: 'canvas',
79
72
  })
80
73
  })
@@ -82,32 +75,32 @@ describe('getResourceIdFromDocumentHandle', () => {
82
75
 
83
76
  describe('with DocumentHandleWithSource - dataset source', () => {
84
77
  it('should return dataset resource ID when dataset source is provided', () => {
85
- const documentHandle: DocumentHandleWithSource = {
78
+ const documentHandle = {
86
79
  documentId: 'test-document-id',
87
80
  documentType: 'test-document-type',
88
- source: datasetSource('source-project-id', 'source-dataset'),
81
+ sourceName: 'dataset',
89
82
  }
90
83
 
91
- const result = getResourceIdFromDocumentHandle(documentHandle)
84
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
92
85
 
93
- expect(result).toEqual({
86
+ expect(result.current).toEqual({
94
87
  id: 'source-project-id.source-dataset',
95
88
  type: undefined,
96
89
  })
97
90
  })
98
91
 
99
92
  it('should use dataset source over projectId/dataset when both are provided', () => {
100
- const documentHandle: DocumentHandleWithSource = {
93
+ const documentHandle = {
101
94
  documentId: 'test-document-id',
102
95
  documentType: 'test-document-type',
103
96
  projectId: 'test-project-id',
104
97
  dataset: 'test-dataset',
105
- source: datasetSource('source-project-id', 'source-dataset'),
98
+ sourceName: 'dataset',
106
99
  }
107
100
 
108
- const result = getResourceIdFromDocumentHandle(documentHandle)
101
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
109
102
 
110
- expect(result).toEqual({
103
+ expect(result.current).toEqual({
111
104
  id: 'source-project-id.source-dataset',
112
105
  type: undefined,
113
106
  })
@@ -116,37 +109,17 @@ describe('getResourceIdFromDocumentHandle', () => {
116
109
 
117
110
  describe('edge cases', () => {
118
111
  it('should handle DocumentHandleWithSource with undefined source', () => {
119
- const documentHandle: DocumentHandleWithSource = {
120
- documentId: 'test-document-id',
121
- documentType: 'test-document-type',
122
- projectId: 'test-project-id',
123
- dataset: 'test-dataset',
124
- source: undefined,
125
- }
126
-
127
- const result = getResourceIdFromDocumentHandle(documentHandle)
128
-
129
- expect(result).toEqual({
130
- id: 'test-project-id.test-dataset',
131
- type: undefined,
132
- })
133
- })
134
-
135
- it('should fall back to projectId/dataset when source is not recognized', () => {
136
- const documentHandle: DocumentHandleWithSource = {
112
+ const documentHandle = {
137
113
  documentId: 'test-document-id',
138
114
  documentType: 'test-document-type',
139
115
  projectId: 'test-project-id',
140
116
  dataset: 'test-dataset',
141
- source: {
142
- __sanity_internal_sourceId: 'unknown-format',
143
- } as unknown as DocumentSource,
117
+ sourceName: undefined,
144
118
  }
145
119
 
146
- const result = getResourceIdFromDocumentHandle(documentHandle)
120
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
147
121
 
148
- // Falls back to projectId.dataset when source format is not recognized
149
- expect(result).toEqual({
122
+ expect(result.current).toEqual({
150
123
  id: 'test-project-id.test-dataset',
151
124
  type: undefined,
152
125
  })
@@ -0,0 +1,46 @@
1
+ import {
2
+ type DocumentHandle,
3
+ isCanvasSource,
4
+ isDatasetSource,
5
+ isMediaLibrarySource,
6
+ } from '@sanity/sdk'
7
+
8
+ import {useNormalizedSourceOptions} from '../../helpers/useNormalizedSourceOptions'
9
+
10
+ interface DashboardMessageResource {
11
+ id: string
12
+ type?: 'media-library' | 'canvas'
13
+ }
14
+ /** Currently only used for dispatching intents to the dashboard,
15
+ * but could easily be extended to other dashboard hooks
16
+ * @beta
17
+ */
18
+ export function useResourceIdFromDocumentHandle(
19
+ documentHandle: DocumentHandle,
20
+ ): DashboardMessageResource {
21
+ const options = useNormalizedSourceOptions(documentHandle)
22
+ const {projectId, dataset, source} = options
23
+ let resourceId: string = ''
24
+ let resourceType: 'media-library' | 'canvas' | undefined
25
+ if (projectId && dataset) {
26
+ resourceId = `${projectId}.${dataset}`
27
+ }
28
+
29
+ if (source) {
30
+ if (isDatasetSource(source)) {
31
+ resourceId = `${source.projectId}.${source.dataset}`
32
+ resourceType = undefined
33
+ } else if (isMediaLibrarySource(source)) {
34
+ resourceId = source.mediaLibraryId
35
+ resourceType = 'media-library'
36
+ } else if (isCanvasSource(source)) {
37
+ resourceId = source.canvasId
38
+ resourceType = 'canvas'
39
+ }
40
+ }
41
+
42
+ return {
43
+ id: resourceId,
44
+ type: resourceType,
45
+ }
46
+ }
@@ -100,6 +100,9 @@ export function useEditDocument<TData>(
100
100
  * 3. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
101
101
  * 4. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
102
102
  *
103
+ * **LiveEdit Documents:**
104
+ * For documents using {@link DocumentHandle.liveEdit | liveEdit mode} (set via `liveEdit: true` in the document handle), edits are applied directly to the published document without creating a draft.
105
+ *
103
106
  * This hook relies on the document state being loaded. If the document is not yet available
104
107
  * (e.g., during initial load), the component using this hook will suspend.
105
108
  *
@@ -23,8 +23,8 @@ export interface DocumentsOptions<
23
23
  TDocumentType extends string = string,
24
24
  TDataset extends string = string,
25
25
  TProjectId extends string = string,
26
- > extends DatasetHandle<TDataset, TProjectId>,
27
- Pick<QueryOptions, 'perspective' | 'params'> {
26
+ >
27
+ extends DatasetHandle<TDataset, TProjectId>, Pick<QueryOptions, 'perspective' | 'params'> {
28
28
  /**
29
29
  * Filter documents by their `_type`. Can be a single type or an array of types.
30
30
  */
@@ -39,6 +39,7 @@ export interface DocumentsOptions<
39
39
  batchSize?: number
40
40
  /**
41
41
  * Sorting configuration for the results
42
+ * @beta
42
43
  */
43
44
  orderings?: SortOrderingItem[]
44
45
  /**
@@ -0,0 +1,85 @@
1
+ import {type DocumentSource} from '@sanity/sdk'
2
+ import {useContext} from 'react'
3
+
4
+ import {SourcesContext} from '../../context/SourcesContext'
5
+
6
+ /**
7
+ * Adds React hook support (sourceName resolution) to core types.
8
+ * This wrapper allows hooks to accept `sourceName` as a convenience,
9
+ * which is then resolved to a `DocumentSource` at the React layer.
10
+ * For now, we are trying to avoid source name resolution in core --
11
+ * functions having sources explicitly passed will reduce complexity.
12
+ *
13
+ * @typeParam T - The core type to extend (must have optional `source` field)
14
+ * @beta
15
+ */
16
+ export type WithSourceNameSupport<T extends {source?: DocumentSource}> = T & {
17
+ /**
18
+ * Optional name of a source to resolve from context.
19
+ * If provided, will be resolved to a `DocumentSource` via `SourcesContext`.
20
+ * @beta
21
+ */
22
+ sourceName?: string
23
+ }
24
+
25
+ /**
26
+ * Normalizes hook options by resolving `sourceName` to a `DocumentSource`.
27
+ * This hook ensures that options passed to core layer functions only contain
28
+ * `source` (never `sourceName`), preventing duplicate cache keys and maintaining
29
+ * clean separation between React and core layers.
30
+ *
31
+ * @typeParam T - The options type (must include optional source field)
32
+ * @param options - Hook options that may include `sourceName` and/or `source`
33
+ * @returns Normalized options with `sourceName` removed and `source` resolved
34
+ *
35
+ * @remarks
36
+ * Resolution priority:
37
+ * 1. If `sourceName` is provided, resolves it via `SourcesContext` and uses that
38
+ * 2. Otherwise, uses the inline `source` if provided
39
+ * 3. If neither is provided, returns options without a source field
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * function useQuery(options: WithSourceNameSupport<QueryOptions>) {
44
+ * const instance = useSanityInstance(options)
45
+ * const normalized = useNormalizedOptions(options)
46
+ * // normalized now has source but never sourceName
47
+ * const queryKey = getQueryKey(normalized)
48
+ * }
49
+ * ```
50
+ *
51
+ * @beta
52
+ */
53
+ export function useNormalizedSourceOptions<
54
+ T extends {source?: DocumentSource; sourceName?: string},
55
+ >(options: T): Omit<T, 'sourceName'> {
56
+ const {sourceName, ...rest} = options
57
+ if (sourceName && Object.hasOwn(options, 'source')) {
58
+ throw new Error(
59
+ `Source name ${JSON.stringify(sourceName)} and source ${JSON.stringify(options.source)} cannot be used together.`,
60
+ )
61
+ }
62
+
63
+ // Resolve sourceName to source via context
64
+ const sources = useContext(SourcesContext)
65
+ let resolvedSource: DocumentSource | undefined
66
+
67
+ if (options.source) {
68
+ resolvedSource = options.source
69
+ }
70
+
71
+ if (sourceName && !Object.hasOwn(sources, sourceName)) {
72
+ throw new Error(
73
+ `There's no source named ${JSON.stringify(sourceName)} in context. Please use <SourceProvider>.`,
74
+ )
75
+ }
76
+
77
+ if (sourceName && sources[sourceName]) {
78
+ resolvedSource = sources[sourceName]
79
+ }
80
+
81
+ return {
82
+ ...rest,
83
+ source: resolvedSource,
84
+ }
85
+ }
@@ -28,6 +28,7 @@ export interface PaginatedDocumentsOptions<
28
28
  pageSize?: number
29
29
  /**
30
30
  * Sorting configuration for the results
31
+ * @beta
31
32
  */
32
33
  orderings?: SortOrderingItem[]
33
34
  /**
@@ -4,6 +4,10 @@ import {useCallback, useMemo, useSyncExternalStore} from 'react'
4
4
  import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
5
5
 
6
6
  import {useSanityInstance} from '../context/useSanityInstance'
7
+ import {
8
+ useNormalizedSourceOptions,
9
+ type WithSourceNameSupport,
10
+ } from '../helpers/useNormalizedSourceOptions'
7
11
 
8
12
  /**
9
13
  * @public
@@ -14,7 +18,7 @@ export interface useDocumentProjectionOptions<
14
18
  TDocumentType extends string = string,
15
19
  TDataset extends string = string,
16
20
  TProjectId extends string = string,
17
- > extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
21
+ > extends WithSourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
18
22
  /** The GROQ projection string */
19
23
  projection: TProjection
20
24
  /** Optional parameters for the projection query */
@@ -183,15 +187,22 @@ export function useDocumentProjection<TData extends object>({
183
187
  // even if the string reference changes (e.g., from inline template literals)
184
188
  const normalizedProjection = useMemo(() => projection.trim(), [projection])
185
189
 
190
+ // Normalize options: resolve sourceName to source and strip sourceName
191
+ const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
192
+
186
193
  // Memoize stateSource based on normalized projection and docHandle properties
187
194
  // This prevents creating a new StateSource on every render when projection content is the same
188
195
  const stateSource = useMemo(
189
- () => getProjectionState<TData>(instance, {...docHandle, projection: normalizedProjection}),
190
- [instance, normalizedProjection, docHandle],
196
+ () =>
197
+ getProjectionState<TData>(instance, {
198
+ ...normalizedDocHandle,
199
+ projection: normalizedProjection,
200
+ }),
201
+ [instance, normalizedDocHandle, normalizedProjection],
191
202
  )
192
203
 
193
204
  if (stateSource.getCurrent()?.data === null) {
194
- throw resolveProjection(instance, {...docHandle, projection: normalizedProjection})
205
+ throw resolveProjection(instance, {...normalizedDocHandle, projection: normalizedProjection})
195
206
  }
196
207
 
197
208
  // Create subscribe function for useSyncExternalStore
@@ -9,6 +9,19 @@ import {type SanityQueryResult} from 'groq'
9
9
  import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {
13
+ useNormalizedSourceOptions,
14
+ type WithSourceNameSupport,
15
+ } from '../helpers/useNormalizedSourceOptions'
16
+ /**
17
+ * Hook options for useQuery, supporting both direct source and sourceName.
18
+ * @beta
19
+ */
20
+ type UseQueryOptions<
21
+ TQuery extends string = string,
22
+ TDataset extends string = string,
23
+ TProjectId extends string = string,
24
+ > = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
12
25
 
13
26
  // Overload 1: Inferred Type (using Typegen)
14
27
  /**
@@ -71,7 +84,7 @@ export function useQuery<
71
84
  TDataset extends string = string,
72
85
  TProjectId extends string = string,
73
86
  >(
74
- options: QueryOptions<TQuery, TDataset, TProjectId>,
87
+ options: UseQueryOptions<TQuery, TDataset, TProjectId>,
75
88
  ): {
76
89
  /** The query result, typed based on the GROQ query string */
77
90
  data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
@@ -108,7 +121,7 @@ export function useQuery<
108
121
  * }
109
122
  * ```
110
123
  */
111
- export function useQuery<TData>(options: QueryOptions): {
124
+ export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
112
125
  /** The query result, cast to the provided type TData */
113
126
  data: TData
114
127
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -133,19 +146,23 @@ export function useQuery<TData>(options: QueryOptions): {
133
146
  *
134
147
  * @category GROQ
135
148
  */
136
- export function useQuery(options: QueryOptions): {data: unknown; isPending: boolean} {
149
+ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
150
+ data: unknown
151
+ isPending: boolean
152
+ } {
137
153
  // Implementation returns unknown, overloads define specifics
138
154
  const instance = useSanityInstance(options)
139
155
 
156
+ // Normalize options: resolve sourceName to source and strip sourceName
157
+ const normalized = useNormalizedSourceOptions(options)
158
+
140
159
  // Use React's useTransition to avoid UI jank when queries change
141
160
  const [isPending, startTransition] = useTransition()
142
161
 
143
- // Get the unique key for this query and its options
144
- const queryKey = getQueryKey(options)
162
+ // Get the unique key for this query and its options (using normalized options)
163
+ const queryKey = getQueryKey(normalized)
145
164
  // Use a deferred state to avoid immediate re-renders when the query changes
146
165
  const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
147
- // Parse the deferred query key back into a query and options
148
- const deferred = useMemo(() => parseQueryKey(deferredQueryKey), [deferredQueryKey])
149
166
 
150
167
  // Create an AbortController to cancel in-flight requests when needed
151
168
  const ref = useRef<AbortController>(new AbortController())
@@ -166,10 +183,11 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
166
183
  }, [deferredQueryKey, queryKey])
167
184
 
168
185
  // Get the state source for this query from the query store
169
- const {getCurrent, subscribe} = useMemo(
170
- () => getQueryState(instance, deferred),
171
- [instance, deferred],
172
- )
186
+ // Memoize the options object by depending on the stable string key instead of the parsed object
187
+ const {getCurrent, subscribe} = useMemo(() => {
188
+ const deferred = parseQueryKey(deferredQueryKey)
189
+ return getQueryState(instance, deferred)
190
+ }, [instance, deferredQueryKey])
173
191
 
174
192
  // If data isn't available yet, suspend rendering
175
193
  if (getCurrent() === undefined) {
@@ -182,6 +200,7 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
182
200
  // the captured signal remains unchanged for this suspended render.
183
201
  // Thus, the promise thrown here uses a stable abort signal, ensuring correct behavior.
184
202
  const currentSignal = ref.current.signal
203
+ const deferred = parseQueryKey(deferredQueryKey)
185
204
 
186
205
  throw resolveQuery(instance, {...deferred, signal: currentSignal})
187
206
  }
@@ -1,12 +0,0 @@
1
- import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
2
- /**
3
- * Document handle that optionally includes a source (e.g., media library source)
4
- * or projectId and dataset for traditional dataset sources
5
- * (but now marked optional since it's valid to just use a source)
6
- * @beta
7
- */
8
- export interface DocumentHandleWithSource extends Omit<DocumentHandle, 'projectId' | 'dataset'> {
9
- source?: DocumentSource
10
- projectId?: string
11
- dataset?: string
12
- }
@@ -1,53 +0,0 @@
1
- import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
2
-
3
- import {type DocumentHandleWithSource} from '../types'
4
-
5
- // Internal constant for accessing source ID
6
- const SOURCE_ID = '__sanity_internal_sourceId' as const
7
-
8
- interface DashboardMessageResource {
9
- id: string
10
- type?: 'media-library' | 'canvas'
11
- }
12
-
13
- const isDocumentHandleWithSource = (
14
- documentHandle: DocumentHandle | DocumentHandleWithSource,
15
- ): documentHandle is DocumentHandleWithSource => {
16
- return 'source' in documentHandle
17
- }
18
-
19
- /** Currently only used for dispatching intents to the dashboard,
20
- * but could easily be extended to other dashboard hooks
21
- * @beta
22
- */
23
- export function getResourceIdFromDocumentHandle(
24
- documentHandle: DocumentHandle | DocumentHandleWithSource,
25
- ): DashboardMessageResource {
26
- let source: DocumentSource | undefined
27
-
28
- const {projectId, dataset} = documentHandle
29
- if (isDocumentHandleWithSource(documentHandle)) {
30
- source = documentHandle.source
31
- }
32
- let resourceId: string = projectId + '.' + dataset
33
- let resourceType: 'media-library' | 'canvas' | undefined
34
-
35
- if (source) {
36
- const sourceId = (source as Record<string, unknown>)[SOURCE_ID]
37
- if (Array.isArray(sourceId)) {
38
- if (sourceId[0] === 'media-library' || sourceId[0] === 'canvas') {
39
- resourceType = sourceId[0] as 'media-library' | 'canvas'
40
- resourceId = sourceId[1] as string
41
- }
42
- } else if (sourceId && typeof sourceId === 'object' && 'projectId' in sourceId) {
43
- const datasetSource = sourceId as {projectId: string; dataset: string}
44
- // don't create type since it's ambiguous for project / dataset docs
45
- resourceId = `${datasetSource.projectId}.${datasetSource.dataset}`
46
- }
47
- }
48
-
49
- return {
50
- id: resourceId,
51
- type: resourceType,
52
- }
53
- }