@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2

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 (168) hide show
  1. package/README.md +103 -155
  2. package/dist/index.d.ts +2488 -2019
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4435 -1529
  5. package/dist/index.js.map +1 -1
  6. package/package.json +55 -59
  7. package/src/_exports/index.ts +2 -4
  8. package/src/_exports/sdk-react.ts +30 -18
  9. package/src/components/SDKProvider.test.tsx +173 -32
  10. package/src/components/SDKProvider.tsx +87 -28
  11. package/src/components/SanityApp.test.tsx +68 -12
  12. package/src/components/SanityApp.tsx +93 -87
  13. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  14. package/src/components/auth/AuthBoundary.test.tsx +7 -7
  15. package/src/components/auth/AuthBoundary.tsx +29 -6
  16. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  17. package/src/components/auth/LoginError.test.tsx +191 -5
  18. package/src/components/auth/LoginError.tsx +104 -61
  19. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  20. package/src/components/errors/ChunkLoadError.tsx +56 -0
  21. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  22. package/src/components/errors/chunkReloadStorage.ts +57 -0
  23. package/src/config/handles.ts +29 -27
  24. package/src/constants.ts +5 -0
  25. package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
  26. package/src/context/ComlinkTokenRefresh.tsx +3 -5
  27. package/src/context/DefaultResourceContext.ts +3 -3
  28. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  29. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  30. package/src/context/PerspectiveContext.ts +3 -3
  31. package/src/context/ProjectContext.ts +15 -0
  32. package/src/context/ResourceProvider.test.tsx +93 -90
  33. package/src/context/ResourceProvider.tsx +62 -93
  34. package/src/context/SDKStudioContext.test.tsx +28 -33
  35. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  36. package/src/context/SanityInstanceProvider.tsx +71 -0
  37. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  38. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  39. package/src/context/renderSanityApp.test.tsx +151 -49
  40. package/src/context/renderSanityApp.tsx +12 -8
  41. package/src/context/workbenchToken.ts +63 -0
  42. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  43. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  44. package/src/hooks/access/useCheckPermissions.ts +24 -0
  45. package/src/hooks/agent/agentActions.test.tsx +2 -2
  46. package/src/hooks/agent/agentActions.ts +52 -72
  47. package/src/hooks/agent/useAgentResourceContext.ts +1 -1
  48. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  49. package/src/hooks/applications/useApplication.ts +22 -0
  50. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  51. package/src/hooks/applications/useApplications.ts +25 -0
  52. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  53. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  54. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  55. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  56. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
  57. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
  58. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  59. package/src/hooks/client/useClient.test.tsx +1 -4
  60. package/src/hooks/client/useClient.ts +1 -0
  61. package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
  62. package/src/hooks/comlink/useFrameConnection.ts +1 -1
  63. package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
  64. package/src/hooks/comlink/useWindowConnection.ts +2 -3
  65. package/src/hooks/comments/useCommentActions.test.tsx +150 -0
  66. package/src/hooks/comments/useCommentActions.ts +109 -0
  67. package/src/hooks/comments/useCommentList.ts +79 -0
  68. package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
  69. package/src/hooks/comments/useCommentThreads.ts +73 -0
  70. package/src/hooks/comments/useComments.test.tsx +242 -0
  71. package/src/hooks/comments/useComments.ts +59 -0
  72. package/src/hooks/context/useResource.test.tsx +32 -0
  73. package/src/hooks/context/useResource.ts +24 -0
  74. package/src/hooks/context/useSanityInstance.test.tsx +18 -1
  75. package/src/hooks/context/useSanityInstance.ts +6 -6
  76. package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
  77. package/src/hooks/dashboard/useManageFavorite.ts +11 -17
  78. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
  79. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
  80. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
  81. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
  82. package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
  83. package/src/hooks/dashboard/useWindowTitle.ts +114 -0
  84. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  85. package/src/hooks/datasets/useDatasets.ts +33 -13
  86. package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
  87. package/src/hooks/document/useApplyDocumentActions.ts +66 -48
  88. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  89. package/src/hooks/document/useCreateDocument.ts +117 -0
  90. package/src/hooks/document/useDocument.test.tsx +37 -8
  91. package/src/hooks/document/useDocument.ts +131 -66
  92. package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
  93. package/src/hooks/document/useDocumentEvent.ts +16 -10
  94. package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
  95. package/src/hooks/document/useDocumentPermissions.ts +71 -39
  96. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
  97. package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
  98. package/src/hooks/document/useEditDocument.test.tsx +21 -14
  99. package/src/hooks/document/useEditDocument.ts +163 -17
  100. package/src/hooks/documents/useDocuments.test.tsx +64 -44
  101. package/src/hooks/documents/useDocuments.ts +31 -20
  102. package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
  103. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  104. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  105. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  106. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  107. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  108. package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
  109. package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
  110. package/src/hooks/helpers/useApplyActions.ts +68 -0
  111. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
  112. package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
  113. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  114. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  115. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  116. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  117. package/src/hooks/installations/useInstallation.ts +22 -0
  118. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  119. package/src/hooks/installations/useInstallations.ts +25 -0
  120. package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
  121. package/src/hooks/organizations/useOrganization.test.ts +63 -0
  122. package/src/hooks/organizations/useOrganization.ts +34 -0
  123. package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
  124. package/src/hooks/organizations/useOrganizations.test.ts +63 -0
  125. package/src/hooks/organizations/useOrganizations.ts +39 -0
  126. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
  127. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
  128. package/src/hooks/presence/usePresence.test.tsx +0 -15
  129. package/src/hooks/presence/usePresence.ts +22 -20
  130. package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
  131. package/src/hooks/presence/usePresenceForDocument.ts +104 -0
  132. package/src/hooks/presence/useReportPresence.test.tsx +202 -0
  133. package/src/hooks/presence/useReportPresence.ts +170 -0
  134. package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
  135. package/src/hooks/preview/useDocumentPreview.tsx +12 -14
  136. package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
  137. package/src/hooks/projection/useDocumentProjection.ts +81 -2
  138. package/src/hooks/projects/useProject.test-d.ts +56 -0
  139. package/src/hooks/projects/useProject.test.tsx +120 -0
  140. package/src/hooks/projects/useProject.ts +42 -43
  141. package/src/hooks/projects/useProjects.test-d.ts +60 -0
  142. package/src/hooks/projects/useProjects.test.ts +45 -98
  143. package/src/hooks/projects/useProjects.ts +17 -32
  144. package/src/hooks/query/useQuery.test.tsx +6 -5
  145. package/src/hooks/query/useQuery.ts +94 -17
  146. package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
  147. package/src/hooks/releases/useActiveReleases.ts +28 -24
  148. package/src/hooks/releases/useAllReleases.test.tsx +93 -0
  149. package/src/hooks/releases/useAllReleases.ts +62 -0
  150. package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
  151. package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
  152. package/src/hooks/releases/usePerspective.test.tsx +18 -10
  153. package/src/hooks/releases/usePerspective.ts +13 -17
  154. package/src/hooks/users/useUser.test.tsx +3 -9
  155. package/src/hooks/users/useUser.ts +4 -8
  156. package/src/hooks/users/useUsers.test.tsx +103 -7
  157. package/src/hooks/users/useUsers.ts +38 -5
  158. package/src/utils/resolveOrgResources.test.ts +111 -0
  159. package/src/utils/resolveOrgResources.ts +69 -0
  160. package/src/hooks/context/useDefaultResource.test.tsx +0 -25
  161. package/src/hooks/context/useDefaultResource.ts +0 -30
  162. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
  163. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  164. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
  165. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
  166. package/src/hooks/datasets/useDatasets.test.ts +0 -66
  167. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
  168. package/src/hooks/projects/useProject.test.ts +0 -79
@@ -1,13 +1,15 @@
1
- import {type DocumentOptions, getDocumentState, resolveDocument} from '@sanity/sdk'
2
- import {type SanityDocument} from '@sanity/types'
1
+ import {type DocumentOptions, getDocumentState, type JsonMatch, resolveDocument} from '@sanity/sdk'
2
+ import {type SanityDocument} from 'groq'
3
+ import {identity} from 'rxjs'
3
4
 
4
5
  import {type DocumentHandle} from '../../config/handles'
5
6
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
6
7
  import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
8
+ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
7
9
  // used in an `{@link useDocumentProjection}` and `{@link useQuery}`
8
- // eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
10
+ // eslint-disable-next-line import-x/consistent-type-specifier-style
9
11
  import type {useDocumentProjection} from '../projection/useDocumentProjection'
10
- // eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
12
+ // eslint-disable-next-line import-x/consistent-type-specifier-style
11
13
  import type {useQuery} from '../query/useQuery'
12
14
 
13
15
  const useDocumentValue = createStateSourceHook({
@@ -20,6 +22,9 @@ const useDocumentValue = createStateSourceHook({
20
22
  // Extract handle part for resolveDocument
21
23
  suspender: (instance, options: DocumentOptions<string | undefined>) =>
22
24
  resolveDocument(instance, options),
25
+ getConfig: identity as (
26
+ options: DocumentOptions<string | undefined>,
27
+ ) => DocumentOptions<string | undefined>,
23
28
  })
24
29
 
25
30
  const wrapHookWithData = <TParams extends unknown[], TReturn>(
@@ -39,108 +44,168 @@ type UseDocumentOptions<
39
44
  > = DocumentHandle<TDocumentType, TDataset, TProjectId> & {path?: TPath}
40
45
 
41
46
  interface UseDocument {
47
+ /** @internal */
48
+ <TDocumentType extends string, TDataset extends string, TProjectId extends string = string>(
49
+ options: UseDocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
50
+ ): {data: SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
51
+
52
+ /** @internal */
53
+ <
54
+ TPath extends string,
55
+ TDocumentType extends string,
56
+ TDataset extends string = string,
57
+ TProjectId extends string = string,
58
+ >(
59
+ options: UseDocumentOptions<TPath, TDocumentType>,
60
+ ): {
61
+ data: JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
62
+ }
63
+
64
+ /** @internal */
65
+ <TData>(options: DocumentOptions<undefined>): {data: TData | null}
66
+ /** @internal */
67
+ <TData>(options: DocumentOptions<string>): {data: TData | undefined}
68
+
42
69
  /**
70
+ * ## useDocument via Type Inference (Recommended)
71
+ *
43
72
  * @public
44
73
  *
45
- * ## useDocument with Explicit Types (no path)
74
+ * The preferred way to use this hook when working with Sanity Typegen.
46
75
  *
47
- * Provide an explicit type parameter `TData` to type the returned document data.
48
- * You can define your own interfaces or use types generated by your Sanity Studio.
76
+ * Features:
77
+ * - Automatically infers document types from your schema
78
+ * - Provides type-safe access to documents and nested fields
79
+ * - Supports project/dataset-specific type inference
80
+ * - Works seamlessly with Typegen-generated types
49
81
  *
50
- * This hook will suspend while the document data is being fetched.
82
+ * This hook will suspend while the document data is being fetched and loaded.
83
+ *
84
+ * When fetching a full document:
85
+ * - Returns the complete document object if it exists
86
+ * - Returns `null` if the document doesn't exist
51
87
  *
52
- * @typeParam TData - The explicit type for the document
88
+ * When fetching with a path:
89
+ * - Returns the value at the specified path if both the document and path exist
90
+ * - Returns `undefined` if either the document doesn't exist or the path doesn't exist in the document
91
+ *
92
+ * @category Documents
53
93
  * @param options - Configuration including `documentId`, `documentType`, and optionally:
94
+ * - `path`: To select a nested value (returns typed value at path)
54
95
  * - `projectId`/`dataset`: For multi-project/dataset setups
55
- * @returns The document state
96
+ * @returns The document state (or nested value if path provided).
56
97
  *
57
- * @example Basic document fetch with explicit type
98
+ * @example Basic document fetch
58
99
  * ```tsx
59
- * import {useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
60
- *
61
- * interface Book extends SanityDocument {
62
- * _type: 'book'
63
- * title: string
64
- * author: string
65
- * }
100
+ * import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
66
101
  *
67
- * interface BookViewProps {
68
- * doc: DocumentHandle
102
+ * interface ProductViewProps {
103
+ * doc: DocumentHandle<'product'> // Typegen infers product type
69
104
  * }
70
105
  *
71
- * function BookView({doc}: BookViewProps) {
72
- * const {data: book} = useDocument<Book>({...doc})
73
- * return <h1>{book?.title ?? 'Untitled'} by {book?.author ?? 'Unknown'}</h1>
106
+ * function ProductView({doc}: ProductViewProps) {
107
+ * const {data: product} = useDocument({...doc}) // Fully typed product
108
+ * return <h1>{product.title ?? 'Untitled'}</h1>
74
109
  * }
75
110
  * ```
76
111
  *
77
- * @inlineType DocumentOptions
78
- */
79
- <TData>(options: UseDocumentOptions<undefined>): {data: TData | null}
80
-
81
- /**
82
- * @public
83
- *
84
- * ## useDocument with Explicit Types (with path)
85
- *
86
- * Provide an explicit type parameter `TData` to type the returned field data
87
- * when using a `path` to select a nested value.
88
- *
89
- * @typeParam TData - The explicit type for the field
90
- * @param options - Configuration including `documentId`, `documentType`, `path`, and optionally:
91
- * - `projectId`/`dataset`: For multi-project/dataset setups
92
- * @returns The field value at the specified path
93
- *
94
- * @example Fetching a specific field with explicit type
112
+ * @example Fetching a specific field
95
113
  * ```tsx
96
114
  * import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
97
115
  *
98
- * function BookTitle({doc}: {doc: DocumentHandle}) {
99
- * const {data: title} = useDocument<string>({...doc, path: 'title'})
116
+ * interface ProductTitleProps {
117
+ * doc: DocumentHandle<'product'>
118
+ * }
119
+ *
120
+ * function ProductTitle({doc}: ProductTitleProps) {
121
+ * const {data: title} = useDocument({
122
+ * ...doc,
123
+ * path: 'title' // Returns just the title field
124
+ * })
100
125
  * return <h1>{title ?? 'Untitled'}</h1>
101
126
  * }
102
127
  * ```
103
128
  *
104
129
  * @inlineType DocumentOptions
105
130
  */
106
- <TData>(options: UseDocumentOptions<string>): {data: TData | undefined}
131
+ <
132
+ TPath extends string | undefined = undefined,
133
+ TDocumentType extends string = string,
134
+ TDataset extends string = string,
135
+ TProjectId extends string = string,
136
+ >(
137
+ options: UseDocumentOptions<TPath, TDocumentType>,
138
+ ): TPath extends string
139
+ ? {
140
+ data:
141
+ | JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>
142
+ | undefined
143
+ }
144
+ : {data: SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
107
145
 
108
146
  /**
109
147
  * @public
110
148
  *
111
- * ## useDocument (default)
149
+ * ## useDocument via Explicit Types
112
150
  *
113
- * When no explicit type parameter is provided, the hook returns `SanityDocument`.
151
+ * Use this version when:
152
+ * - You're not using Sanity Typegen
153
+ * - You need to manually specify document types
154
+ * - You're working with dynamic document types
114
155
  *
115
- * This hook will suspend while the document data is being fetched and loaded.
156
+ * Key differences from Typegen version:
157
+ * - Requires manual type specification via `TData`
158
+ * - Returns `TData | null` for full documents
159
+ * - Returns `TData | undefined` for nested values
116
160
  *
117
- * When fetching a full document:
118
- * - Returns the complete document object if it exists
119
- * - Returns `null` if the document doesn't exist
120
- *
121
- * When fetching with a path:
122
- * - Returns the value at the specified path if both the document and path exist
123
- * - Returns `undefined` if either the document doesn't exist or the path doesn't exist in the document
161
+ * This hook will suspend while the document data is being fetched.
124
162
  *
125
- * @category Documents
126
- * @param options - Configuration including `documentId`, `documentType`, and optionally:
163
+ * @typeParam TData - The explicit type for the document or field
164
+ * @typeParam TPath - Optional path to a nested value
165
+ * @param options - Configuration including `documentId` and optionally:
127
166
  * - `path`: To select a nested value
128
167
  * - `projectId`/`dataset`: For multi-project/dataset setups
129
- * @returns The document state (or nested value if path provided).
168
+ * @returns The document state (or nested value if path provided)
130
169
  *
131
- * @example Basic document fetch
170
+ * @example Basic document fetch with explicit type
171
+ * ```tsx
172
+ * import {useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
173
+ *
174
+ * interface Book extends SanityDocument {
175
+ * _type: 'book'
176
+ * title: string
177
+ * author: string
178
+ * }
179
+ *
180
+ * interface BookViewProps {
181
+ * doc: DocumentHandle
182
+ * }
183
+ *
184
+ * function BookView({doc}: BookViewProps) {
185
+ * const {data: book} = useDocument<Book>({...doc})
186
+ * return <h1>{book?.title ?? 'Untitled'} by {book?.author ?? 'Unknown'}</h1>
187
+ * }
188
+ * ```
189
+ *
190
+ * @example Fetching a specific field with explicit type
132
191
  * ```tsx
133
192
  * import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
134
193
  *
135
- * function ProductView({doc}: {doc: DocumentHandle}) {
136
- * const {data: product} = useDocument({...doc})
137
- * return <h1>{product?.title ?? 'Untitled'}</h1>
194
+ * interface BookTitleProps {
195
+ * doc: DocumentHandle
196
+ * }
197
+ *
198
+ * function BookTitle({doc}: BookTitleProps) {
199
+ * const {data: title} = useDocument<string>({...doc, path: 'title'})
200
+ * return <h1>{title ?? 'Untitled'}</h1>
138
201
  * }
139
202
  * ```
140
203
  *
141
204
  * @inlineType DocumentOptions
142
205
  */
143
- (options: UseDocumentOptions<undefined>): {data: SanityDocument | null}
206
+ <TData, TPath extends string>(
207
+ options: UseDocumentOptions<TPath>,
208
+ ): TPath extends string ? {data: TData | undefined} : {data: TData | null}
144
209
 
145
210
  /**
146
211
  * @internal
@@ -152,11 +217,10 @@ interface UseDocument {
152
217
  * @public
153
218
  * Reads and subscribes to a document's realtime state, incorporating both local and remote changes.
154
219
  *
155
- * You can provide an explicit type parameter for full type safety:
156
- * - `useDocument<MyType>({...})` returns `{data: MyType | null}`
157
- * - `useDocument<string>({..., path: 'title'})` returns `{data: string | undefined}`
220
+ * This hook comes in two main flavors to suit your needs:
158
221
  *
159
- * Without a type parameter, data is typed as `SanityDocument`.
222
+ * 1. **[Type Inference](#usedocument-via-type-inference-recommended)** (Recommended) - Automatically gets types from your Sanity schema
223
+ * 2. **[Explicit Types](#usedocument-via-explicit-types)** - Manually specify types when needed
160
224
  *
161
225
  * @remarks
162
226
  * `useDocument` is ideal for realtime editing interfaces where you need immediate feedback on changes.
@@ -173,6 +237,7 @@ interface UseDocument {
173
237
  * @function
174
238
  */
175
239
  export const useDocument = wrapHookWithData((options: UseDocumentOptions) => {
240
+ useTrackHookUsage('useDocument')
176
241
  const normalizedOptions = useNormalizedResourceOptions(options)
177
242
  return useDocumentValue(normalizedOptions)
178
243
  }) as UseDocument
@@ -1,8 +1,9 @@
1
1
  // tests/useDocumentEvent.test.ts
2
2
  import {type DocumentEvent, type DocumentHandle, subscribeDocumentEvents} from '@sanity/sdk'
3
+ import {renderHook} from '@testing-library/react'
3
4
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
5
 
5
- import {renderHook} from '../../../test/test-utils'
6
+ import {ResourceProvider} from '../../context/ResourceProvider'
6
7
  import {useDocumentEvent} from './useDocumentEvent'
7
8
 
8
9
  vi.mock('@sanity/sdk', async (importOriginal) => {
@@ -13,7 +14,6 @@ vi.mock('@sanity/sdk', async (importOriginal) => {
13
14
  const docHandle: DocumentHandle = {
14
15
  documentId: 'doc1',
15
16
  documentType: 'book',
16
- resource: {projectId: 'p', dataset: 'd'},
17
17
  }
18
18
 
19
19
  describe('useDocumentEvent hook', () => {
@@ -26,7 +26,13 @@ describe('useDocumentEvent hook', () => {
26
26
  const unsubscribe = vi.fn()
27
27
  vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
28
28
 
29
- renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}))
29
+ renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}), {
30
+ wrapper: ({children}) => (
31
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
32
+ {children}
33
+ </ResourceProvider>
34
+ ),
35
+ })
30
36
 
31
37
  expect(vi.mocked(subscribeDocumentEvents)).toHaveBeenCalledTimes(1)
32
38
  expect(vi.mocked(subscribeDocumentEvents).mock.calls[0][0]).toEqual(expect.any(Object))
@@ -44,7 +50,13 @@ describe('useDocumentEvent hook', () => {
44
50
  const unsubscribe = vi.fn()
45
51
  vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
46
52
 
47
- const {unmount} = renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}))
53
+ const {unmount} = renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}), {
54
+ wrapper: ({children}) => (
55
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
56
+ {children}
57
+ </ResourceProvider>
58
+ ),
59
+ })
48
60
  unmount()
49
61
  expect(unsubscribe).toHaveBeenCalledTimes(1)
50
62
  })
@@ -4,6 +4,7 @@ import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
4
4
  import {type ResourceHandle} from '../../config/handles'
5
5
  import {useSanityInstance} from '../context/useSanityInstance'
6
6
  import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
7
+ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
7
8
 
8
9
  /**
9
10
  * @public
@@ -11,7 +12,7 @@ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOpti
11
12
  export interface UseDocumentEventOptions<
12
13
  TDataset extends string = string,
13
14
  TProjectId extends string = string,
14
- > extends ResourceHandle<TProjectId, TDataset> {
15
+ > extends ResourceHandle<TDataset, TProjectId> {
15
16
  onEvent: (documentEvent: DocumentEvent) => void
16
17
  }
17
18
 
@@ -22,19 +23,19 @@ export interface UseDocumentEventOptions<
22
23
  * Subscribes an event handler to events in your application's document store.
23
24
  *
24
25
  * @category Documents
25
- * @param options - An object containing the event handler (`onEvent`) and optionally a `resource`. If no resource is provided, the nearest resource from context will be used.
26
+ * @param options - An object containing the event handler (`onEvent`) and optionally a `DatasetHandle` (projectId and dataset). If the handle is not provided, the nearest Sanity instance from context will be used.
26
27
  * @example Creating a custom hook for document event toasts
27
28
  * ```tsx
28
- * import {type DocumentResource, type DocumentEvent, useDocumentEvent} from '@sanity/sdk-react'
29
+ * import {createDatasetHandle, type DatasetHandle, type DocumentEvent, useDocumentEvent} from '@sanity/sdk-react'
29
30
  * import {useToast} from './my-ui-library'
30
31
  *
31
- * // Define options for the custom hook
32
- * interface DocumentToastsOptions {
33
- * resource?: DocumentResource
32
+ * // Define options for the custom hook, extending DatasetHandle
33
+ * interface DocumentToastsOptions extends DatasetHandle {
34
+ * // Could add more options, e.g., { includeEvents: DocumentEvent['type'][] }
34
35
  * }
35
36
  *
36
37
  * // Define the custom hook
37
- * function useDocumentToasts({resource}: DocumentToastsOptions = {}) {
38
+ * function useDocumentToasts({...datasetHandle}: DocumentToastsOptions = {}) {
38
39
  * const showToast = useToast() // Get the toast function
39
40
  *
40
41
  * // Define the event handler logic to show toasts on specific events
@@ -46,20 +47,24 @@ export interface UseDocumentEventOptions<
46
47
  * } else if (event.type === 'deleted') {
47
48
  * showToast(`Document ${event.documentId} deleted.`)
48
49
  * } else {
50
+ * // Optionally log other events for debugging
49
51
  * console.log('Document Event:', event.type, event.documentId)
50
52
  * }
51
53
  * }
52
54
  *
55
+ * // Call the original hook, spreading the handle properties
53
56
  * useDocumentEvent({
54
- * resource,
57
+ * ...datasetHandle, // Spread the dataset handle (projectId, dataset)
55
58
  * onEvent: handleEvent,
56
59
  * })
57
60
  * }
58
61
  *
59
62
  * function MyComponentWithToasts() {
60
- * useDocumentToasts({resource: {projectId: 'p1', dataset: 'ds1'}})
63
+ * // Use the custom hook, passing specific handle info
64
+ * const specificHandle = createDatasetHandle({ projectId: 'p1', dataset: 'ds1' })
65
+ * useDocumentToasts(specificHandle)
61
66
  *
62
- * // Or rely on context for the resource:
67
+ * // // Or use it relying on context for the handle
63
68
  * // useDocumentToasts()
64
69
  *
65
70
  * return <div>...</div>
@@ -73,6 +78,7 @@ export function useDocumentEvent<
73
78
  // Single options object parameter
74
79
  options: UseDocumentEventOptions<TDataset, TProjectId>,
75
80
  ): void {
81
+ useTrackHookUsage('useDocumentEvent')
76
82
  // Destructure handler and datasetHandle from options
77
83
  const normalizedOptions = useNormalizedResourceOptions(options)
78
84
  const {onEvent, ...datasetHandle} = normalizedOptions
@@ -1,6 +1,7 @@
1
1
  import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
2
+ import {renderHook as reactRenderHook} from '@testing-library/react'
2
3
  import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'
3
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
4
5
 
5
6
  import {act, renderHook, waitFor} from '../../../test/test-utils'
6
7
  import {ResourceProvider} from '../../context/ResourceProvider'
@@ -46,8 +47,8 @@ describe('usePermissions', () => {
46
47
  }
47
48
 
48
49
  let permissionsSubject: BehaviorSubject<DocumentPermissionsResult | undefined>
49
- let mockSubscribe: ReturnType<typeof vi.fn>
50
- let mockGetCurrent: ReturnType<typeof vi.fn>
50
+ let mockSubscribe: Mock<(onStoreChanged?: () => void) => () => void>
51
+ let mockGetCurrent: Mock<() => DocumentPermissionsResult | undefined>
51
52
 
52
53
  beforeEach(() => {
53
54
  vi.clearAllMocks()
@@ -71,7 +72,7 @@ describe('usePermissions', () => {
71
72
  observable:
72
73
  permissionsSubject.asObservable() as unknown as Observable<DocumentPermissionsResult>,
73
74
  subscribe: mockSubscribe,
74
- getCurrent: mockGetCurrent,
75
+ getCurrent: mockGetCurrent as unknown as () => DocumentPermissionsResult,
75
76
  })
76
77
  })
77
78
 
@@ -177,7 +178,7 @@ describe('usePermissions', () => {
177
178
  vi.mocked(firstValueFrom).mockReturnValueOnce(mockPromise)
178
179
 
179
180
  // This should throw the promise and suspend
180
- const {result} = renderHook(
181
+ const {result} = reactRenderHook(
181
182
  () => {
182
183
  try {
183
184
  return useDocumentPermissions(mockAction)
@@ -216,7 +217,7 @@ describe('usePermissions', () => {
216
217
  it('throws when no resource is found from action or context', () => {
217
218
  // Provide SanityInstance via ResourceProvider but no resource, so contextResource is undefined
218
219
  expect(() => {
219
- renderHook(
220
+ reactRenderHook(
220
221
  () =>
221
222
  useDocumentPermissions({
222
223
  type: 'document.publish',
@@ -228,7 +229,7 @@ describe('usePermissions', () => {
228
229
  wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
229
230
  },
230
231
  )
231
- }).toThrow(/resource is required/)
232
+ }).toThrow(/No resource found/)
232
233
  })
233
234
 
234
235
  it('should react to permission state changes', async () => {
@@ -1,11 +1,19 @@
1
1
  import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
2
+ import {isDeepEqual} from '@sanity/sdk/_internal'
2
3
  import {useCallback, useContext, useMemo, useSyncExternalStore} from 'react'
3
4
  import {filter, firstValueFrom} from 'rxjs'
4
5
 
5
- import {ResourceContext} from '../../context/DefaultResourceContext'
6
6
  import {ResourcesContext} from '../../context/ResourcesContext'
7
7
  import {useSanityInstance} from '../context/useSanityInstance'
8
- import {normalizeResourceOptions} from '../helpers/useNormalizedResourceOptions'
8
+ import {
9
+ normalizeResourceOptions,
10
+ useEffectiveContextResource,
11
+ type WithResourceNameSupport,
12
+ } from '../helpers/useNormalizedResourceOptions'
13
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
14
+
15
+ const noopSubscribe = () => () => {}
16
+ const returnUndefined = () => undefined
9
17
 
10
18
  /**
11
19
  *
@@ -85,63 +93,87 @@ import {normalizeResourceOptions} from '../helpers/useNormalizedResourceOptions'
85
93
  * ```
86
94
  */
87
95
  export function useDocumentPermissions(
88
- actionOrActions: DocumentAction | DocumentAction[],
96
+ actionOrActions:
97
+ | WithResourceNameSupport<DocumentAction>
98
+ | WithResourceNameSupport<DocumentAction>[],
89
99
  ): DocumentPermissionsResult {
90
100
  const instance = useSanityInstance()
91
- const contextResource = useContext(ResourceContext)
101
+ trackHookUsage(instance, 'useDocumentPermissions')
102
+ const effectiveContextResource = useEffectiveContextResource()
92
103
  const resources = useContext(ResourcesContext)
93
104
 
94
- const normalizedActions = useMemo(() => {
95
- return Array.isArray(actionOrActions)
105
+ const {
106
+ actions: normalizedActions,
107
+ resource: actionResource,
108
+ error: validationError,
109
+ } = useMemo(() => {
110
+ const normalized = Array.isArray(actionOrActions)
96
111
  ? actionOrActions.map((action) =>
97
- normalizeResourceOptions(action, resources, contextResource),
112
+ normalizeResourceOptions(action, resources, effectiveContextResource),
98
113
  )
99
- : [normalizeResourceOptions(actionOrActions, resources, contextResource)]
100
- }, [actionOrActions, resources, contextResource])
101
-
102
- // if actions is an array, we need to check that all actions belong to the same resource
103
- let resource
114
+ : [normalizeResourceOptions(actionOrActions, resources, effectiveContextResource)]
104
115
 
105
- for (const action of normalizedActions) {
106
- if (action.resource) {
107
- if (!resource) resource = action.resource
108
- if (action.resource !== resource) {
109
- throw new Error(
110
- `Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
111
- )
116
+ let resource
117
+ for (const action of normalized) {
118
+ if (action.resource) {
119
+ if (!resource) resource = action.resource
120
+ if (!isDeepEqual(action.resource, resource)) {
121
+ return {
122
+ actions: normalized,
123
+ resource,
124
+ error: new Error(
125
+ `Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
126
+ ),
127
+ }
128
+ }
112
129
  }
113
130
  }
114
- }
131
+ return {actions: normalized, resource, error: undefined}
132
+ }, [actionOrActions, resources, effectiveContextResource])
115
133
 
116
- const effectiveResource = resource ?? contextResource
117
-
118
- if (!effectiveResource) {
119
- throw new Error(
120
- 'No resource found. Provide a resource via the action handle or wrap with a resource context.',
121
- )
122
- }
134
+ const effectiveResource = actionResource ?? effectiveContextResource
123
135
 
136
+ // Keep hooks unconditional — validation errors and missing-resource errors are
137
+ // thrown after all hooks so that the hook call count stays stable across renders.
124
138
  const permissionsOptions = useMemo(
125
- () => ({resource: effectiveResource, actions: normalizedActions as DocumentAction[]}),
139
+ () =>
140
+ effectiveResource
141
+ ? {
142
+ resource: effectiveResource,
143
+ // `Omit<>` on `DocumentAction` loses the discriminant; runtime values are still actions.
144
+ actions: normalizedActions as DocumentAction[],
145
+ }
146
+ : undefined,
126
147
  [effectiveResource, normalizedActions],
127
148
  )
128
149
 
129
- const isDocumentReady = useCallback(
130
- () => getPermissionsState(instance, permissionsOptions).getCurrent() !== undefined,
150
+ const stateSource = useMemo(
151
+ () => (permissionsOptions ? getPermissionsState(instance, permissionsOptions) : undefined),
131
152
  [permissionsOptions, instance],
132
153
  )
154
+
155
+ const isDocumentReady = useCallback(
156
+ () => stateSource !== undefined && stateSource.getCurrent() !== undefined,
157
+ [stateSource],
158
+ )
159
+
160
+ const result = useSyncExternalStore(
161
+ stateSource?.subscribe ?? noopSubscribe,
162
+ stateSource?.getCurrent ?? returnUndefined,
163
+ )
164
+
165
+ // All hooks have been called — safe to throw now.
166
+ if (validationError) throw validationError
167
+ if (!effectiveResource) {
168
+ throw new Error(
169
+ 'No resource found. Provide a resource via the action handle or wrap with a resource context.',
170
+ )
171
+ }
133
172
  if (!isDocumentReady()) {
134
173
  throw firstValueFrom(
135
- getPermissionsState(instance, permissionsOptions).observable.pipe(
136
- filter((result) => result !== undefined),
137
- ),
174
+ stateSource!.observable.pipe(filter((permissions) => permissions !== undefined)),
138
175
  )
139
176
  }
140
177
 
141
- const {subscribe, getCurrent} = useMemo(
142
- () => getPermissionsState(instance, permissionsOptions),
143
- [permissionsOptions, instance],
144
- )
145
-
146
- return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
178
+ return result as DocumentPermissionsResult
147
179
  }
@@ -25,6 +25,7 @@ describe('useDocumentSyncStatus', () => {
25
25
  getState: getDocumentSyncStatus,
26
26
  shouldSuspend: expect.any(Function),
27
27
  suspender: expect.any(Function),
28
+ getConfig: expect.any(Function),
28
29
  }),
29
30
  )
30
31
  // Verify that the hook was created and can be called