@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.31

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 (131) hide show
  1. package/README.md +6 -100
  2. package/dist/index.d.ts +2390 -2
  3. package/dist/index.js +1119 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +35 -49
  6. package/src/_exports/index.ts +2 -10
  7. package/src/_exports/sdk-react.ts +73 -0
  8. package/src/components/SDKProvider.test.tsx +103 -0
  9. package/src/components/SDKProvider.tsx +52 -0
  10. package/src/components/SanityApp.test.tsx +244 -0
  11. package/src/components/SanityApp.tsx +106 -0
  12. package/src/components/auth/AuthBoundary.test.tsx +204 -29
  13. package/src/components/auth/AuthBoundary.tsx +96 -19
  14. package/src/components/auth/ConfigurationError.ts +22 -0
  15. package/src/components/auth/LoginCallback.test.tsx +22 -24
  16. package/src/components/auth/LoginCallback.tsx +6 -16
  17. package/src/components/auth/LoginError.test.tsx +11 -18
  18. package/src/components/auth/LoginError.tsx +43 -25
  19. package/src/components/utils.ts +22 -0
  20. package/src/context/ResourceProvider.test.tsx +157 -0
  21. package/src/context/ResourceProvider.tsx +111 -0
  22. package/src/context/SanityInstanceContext.ts +4 -0
  23. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  24. package/src/hooks/auth/useAuthState.tsx +4 -5
  25. package/src/hooks/auth/useAuthToken.tsx +1 -1
  26. package/src/hooks/auth/useCurrentUser.tsx +28 -4
  27. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  28. package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
  29. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  30. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
  31. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  32. package/src/hooks/auth/useLogOut.tsx +1 -1
  33. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  34. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  35. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  36. package/src/hooks/client/useClient.ts +13 -33
  37. package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
  38. package/src/hooks/comlink/useFrameConnection.ts +107 -0
  39. package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
  40. package/src/hooks/comlink/useManageFavorite.ts +210 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
  42. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
  43. package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
  44. package/src/hooks/comlink/useWindowConnection.ts +123 -0
  45. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  46. package/src/hooks/context/useSanityInstance.ts +68 -11
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
  48. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
  50. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
  51. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  52. package/src/hooks/datasets/useDatasets.ts +52 -0
  53. package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
  54. package/src/hooks/document/useApplyDocumentActions.ts +124 -0
  55. package/src/hooks/document/useDocument.test.ts +118 -0
  56. package/src/hooks/document/useDocument.ts +212 -0
  57. package/src/hooks/document/useDocumentEvent.test.ts +62 -0
  58. package/src/hooks/document/useDocumentEvent.ts +94 -0
  59. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  60. package/src/hooks/document/useDocumentPermissions.ts +131 -0
  61. package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
  62. package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
  63. package/src/hooks/document/useEditDocument.test.ts +196 -0
  64. package/src/hooks/document/useEditDocument.ts +314 -0
  65. package/src/hooks/documents/useDocuments.test.tsx +179 -0
  66. package/src/hooks/documents/useDocuments.ts +300 -0
  67. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  68. package/src/hooks/helpers/createCallbackHook.tsx +1 -1
  69. package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
  70. package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
  71. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
  72. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
  73. package/src/hooks/preview/usePreview.test.tsx +85 -17
  74. package/src/hooks/preview/usePreview.tsx +81 -22
  75. package/src/hooks/projection/useProjection.test.tsx +283 -0
  76. package/src/hooks/projection/useProjection.ts +232 -0
  77. package/src/hooks/projects/useProject.test.ts +80 -0
  78. package/src/hooks/projects/useProject.ts +51 -0
  79. package/src/hooks/projects/useProjects.test.ts +77 -0
  80. package/src/hooks/projects/useProjects.ts +45 -0
  81. package/src/hooks/query/useQuery.test.tsx +188 -0
  82. package/src/hooks/query/useQuery.ts +193 -0
  83. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  84. package/src/hooks/releases/useActiveReleases.ts +39 -0
  85. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  86. package/src/hooks/releases/usePerspective.ts +49 -0
  87. package/src/hooks/users/useUsers.test.tsx +330 -0
  88. package/src/hooks/users/useUsers.ts +120 -0
  89. package/src/utils/getEnv.ts +21 -0
  90. package/src/version.ts +8 -0
  91. package/src/vite-env.d.ts +10 -0
  92. package/dist/_chunks-es/useLogOut.js +0 -44
  93. package/dist/_chunks-es/useLogOut.js.map +0 -1
  94. package/dist/assets/bundle-CcAyERuZ.css +0 -11
  95. package/dist/components.d.ts +0 -259
  96. package/dist/components.js +0 -301
  97. package/dist/components.js.map +0 -1
  98. package/dist/hooks.d.ts +0 -186
  99. package/dist/hooks.js +0 -81
  100. package/dist/hooks.js.map +0 -1
  101. package/src/_exports/components.ts +0 -13
  102. package/src/_exports/hooks.ts +0 -9
  103. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
  104. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  105. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
  106. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
  107. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  108. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
  109. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  110. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
  111. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  112. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
  113. package/src/components/Login/LoginLinks.test.tsx +0 -100
  114. package/src/components/Login/LoginLinks.tsx +0 -73
  115. package/src/components/auth/Login.test.tsx +0 -41
  116. package/src/components/auth/Login.tsx +0 -45
  117. package/src/components/auth/LoginFooter.test.tsx +0 -29
  118. package/src/components/auth/LoginFooter.tsx +0 -65
  119. package/src/components/auth/LoginLayout.test.tsx +0 -33
  120. package/src/components/auth/LoginLayout.tsx +0 -81
  121. package/src/components/context/SanityProvider.test.tsx +0 -25
  122. package/src/components/context/SanityProvider.tsx +0 -42
  123. package/src/css/css.config.js +0 -220
  124. package/src/css/paramour.css +0 -2347
  125. package/src/css/styles.css +0 -11
  126. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  127. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  128. package/src/hooks/auth/useLoginUrls.tsx +0 -51
  129. package/src/hooks/client/useClient.test.tsx +0 -130
  130. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  131. package/src/hooks/documentCollection/useDocuments.ts +0 -87
@@ -0,0 +1,300 @@
1
+ import {
2
+ createGroqSearchFilter,
3
+ type DatasetHandle,
4
+ type DocumentHandle,
5
+ type QueryOptions,
6
+ } from '@sanity/sdk'
7
+ import {type SortOrderingItem} from '@sanity/types'
8
+ import {pick} from 'lodash-es'
9
+ import {useCallback, useEffect, useMemo, useState} from 'react'
10
+
11
+ import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {useQuery} from '../query/useQuery'
13
+
14
+ const DEFAULT_BATCH_SIZE = 25
15
+
16
+ /**
17
+ * Configuration options for the useDocuments hook
18
+ *
19
+ * @beta
20
+ * @category Types
21
+ */
22
+ export interface DocumentsOptions<
23
+ TDocumentType extends string = string,
24
+ TDataset extends string = string,
25
+ TProjectId extends string = string,
26
+ > extends DatasetHandle<TDataset, TProjectId>,
27
+ Pick<QueryOptions, 'perspective' | 'params'> {
28
+ /**
29
+ * Filter documents by their `_type`. Can be a single type or an array of types.
30
+ */
31
+ documentType?: TDocumentType | TDocumentType[]
32
+ /**
33
+ * GROQ filter expression to apply to the query
34
+ */
35
+ filter?: string
36
+ /**
37
+ * Number of items to load per batch (defaults to 25)
38
+ */
39
+ batchSize?: number
40
+ /**
41
+ * Sorting configuration for the results
42
+ */
43
+ orderings?: SortOrderingItem[]
44
+ /**
45
+ * Text search query to filter results
46
+ */
47
+ search?: string
48
+ }
49
+
50
+ /**
51
+ * Return value from the useDocuments hook
52
+ *
53
+ * @beta
54
+ * @category Types
55
+ */
56
+ export interface DocumentsResponse<
57
+ TDocumentType extends string = string,
58
+ TDataset extends string = string,
59
+ TProjectId extends string = string,
60
+ > {
61
+ /**
62
+ * Array of document handles for the current batch
63
+ */
64
+ data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]
65
+ /**
66
+ * Whether there are more items available to load
67
+ */
68
+ hasMore: boolean
69
+ /**
70
+ * Total count of items matching the query
71
+ */
72
+ count: number
73
+ /**
74
+ * Whether a query is currently in progress
75
+ */
76
+ isPending: boolean
77
+ /**
78
+ * Function to load the next batch of results
79
+ */
80
+ loadMore: () => void
81
+ }
82
+
83
+ /**
84
+ * Retrieves batches of {@link DocumentHandle}s, narrowed by optional filters, text searches, and custom ordering,
85
+ * with infinite scrolling support. The number of document handles returned per batch is customizable,
86
+ * and additional batches can be loaded using the supplied `loadMore` function.
87
+ *
88
+ * @beta
89
+ * @category Documents
90
+ * @param options - Configuration options for the infinite list
91
+ * @returns An object containing the list of document handles, the loading state, the total count of retrieved document handles, and a function to load more
92
+ *
93
+ * @remarks
94
+ * - The returned document handles include projectId and dataset information from the current Sanity instance
95
+ * - This makes them ready to use with document operations and other document hooks
96
+ * - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
97
+ *
98
+ * @example Basic infinite list with loading more
99
+ * ```tsx
100
+ * import {
101
+ * useDocuments,
102
+ * createDatasetHandle,
103
+ * type DatasetHandle,
104
+ * type DocumentHandle,
105
+ * type SortOrderingItem
106
+ * } from '@sanity/sdk-react'
107
+ * import {Suspense} from 'react'
108
+ *
109
+ * // Define a component to display a single document (using useProjection for efficiency)
110
+ * function MyDocumentComponent({doc}: {doc: DocumentHandle}) {
111
+ * const {data} = useProjection<{title?: string}>({
112
+ * ...doc, // Pass the full handle
113
+ * projection: '{title}'
114
+ * })
115
+ *
116
+ * return <>{data?.title || 'Untitled'}</>
117
+ * }
118
+ *
119
+ * // Define props for the list component
120
+ * interface DocumentListProps {
121
+ * dataset: DatasetHandle
122
+ * documentType: string
123
+ * search?: string
124
+ * }
125
+ *
126
+ * function DocumentList({dataset, documentType, search}: DocumentListProps) {
127
+ * const { data, hasMore, isPending, loadMore, count } = useDocuments({
128
+ * ...dataset,
129
+ * documentType,
130
+ * search,
131
+ * batchSize: 10,
132
+ * orderings: [{field: '_createdAt', direction: 'desc'}],
133
+ * })
134
+ *
135
+ * return (
136
+ * <div>
137
+ * <p>Total documents: {count}</p>
138
+ * <ol>
139
+ * {data.map((docHandle) => (
140
+ * <li key={docHandle.documentId}>
141
+ * <Suspense fallback="Loading…">
142
+ * <MyDocumentComponent docHandle={docHandle} />
143
+ * </Suspense>
144
+ * </li>
145
+ * ))}
146
+ * </ol>
147
+ * {hasMore && (
148
+ * <button onClick={loadMore}>
149
+ * {isPending ? 'Loading...' : 'Load More'}
150
+ * </button>
151
+ * )}
152
+ * </div>
153
+ * )
154
+ * }
155
+ *
156
+ * // Usage:
157
+ * // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
158
+ * // <DocumentList dataset={myDatasetHandle} documentType="post" search="Sanity" />
159
+ * ```
160
+ *
161
+ * @example Using `filter` and `params` options for narrowing a collection
162
+ * ```tsx
163
+ * import {useState} from 'react'
164
+ * import {useDocuments} from '@sanity/sdk-react'
165
+ *
166
+ * export default function FilteredAuthors() {
167
+ * const [max, setMax] = useState(2)
168
+ * const {data} = useDocuments({
169
+ * documentType: 'author',
170
+ * filter: 'length(books) <= $max',
171
+ * params: {max},
172
+ * })
173
+ *
174
+ * return (
175
+ * <>
176
+ * <input
177
+ * id="maxBooks"
178
+ * type="number"
179
+ * value={max}
180
+ * onChange={e => setMax(e.currentTarget.value)}
181
+ * />
182
+ * {data.map(author => (
183
+ * <Suspense key={author.documentId}>
184
+ * <MyAuthorComponent documentHandle={author} />
185
+ * </Suspense>
186
+ * ))}
187
+ * </>
188
+ * )
189
+ * }
190
+ * ```
191
+ */
192
+ export function useDocuments<
193
+ TDocumentType extends string = string,
194
+ TDataset extends string = string,
195
+ TProjectId extends string = string,
196
+ >({
197
+ batchSize = DEFAULT_BATCH_SIZE,
198
+ params,
199
+ search,
200
+ filter,
201
+ orderings,
202
+ documentType,
203
+ ...options
204
+ }: DocumentsOptions<TDocumentType, TDataset, TProjectId>): DocumentsResponse<
205
+ TDocumentType,
206
+ TDataset,
207
+ TProjectId
208
+ > {
209
+ const instance = useSanityInstance(options)
210
+ const [limit, setLimit] = useState(batchSize)
211
+ const documentTypes = useMemo(
212
+ () =>
213
+ (Array.isArray(documentType) ? documentType : [documentType]).filter(
214
+ (i): i is TDocumentType => typeof i === 'string',
215
+ ),
216
+ [documentType],
217
+ )
218
+
219
+ // Reset the limit to the current batchSize whenever any query parameters
220
+ // (filter, search, params, orderings) or batchSize changes
221
+ const key = JSON.stringify({
222
+ filter,
223
+ search,
224
+ params,
225
+ orderings,
226
+ batchSize,
227
+ types: documentTypes,
228
+ ...options,
229
+ })
230
+ useEffect(() => {
231
+ setLimit(batchSize)
232
+ }, [key, batchSize])
233
+
234
+ const filterClause = useMemo(() => {
235
+ const conditions: string[] = []
236
+ const trimmedSearch = search?.trim()
237
+
238
+ // Add search query filter if specified
239
+ if (trimmedSearch) {
240
+ const searchFilter = createGroqSearchFilter(trimmedSearch)
241
+ if (searchFilter) {
242
+ conditions.push(searchFilter)
243
+ }
244
+ }
245
+
246
+ // Add type filter if specified
247
+ if (documentTypes?.length) {
248
+ conditions.push(`(_type in $__types)`)
249
+ }
250
+
251
+ // Add additional filter if specified
252
+ if (filter) {
253
+ conditions.push(`(${filter})`)
254
+ }
255
+
256
+ return conditions.length ? `[${conditions.join(' && ')}]` : ''
257
+ }, [filter, search, documentTypes])
258
+
259
+ const orderClause = orderings
260
+ ? `| order(${orderings
261
+ .map((ordering) =>
262
+ [ordering.field, ordering.direction.toLowerCase()]
263
+ .map((str) => str.trim())
264
+ .filter(Boolean)
265
+ .join(' '),
266
+ )
267
+ .join(',')})`
268
+ : ''
269
+
270
+ const dataQuery = `*${filterClause}${orderClause}[0...${limit}]{"documentId":_id,"documentType":_type,...$__handle}`
271
+ const countQuery = `count(*${filterClause})`
272
+
273
+ const {
274
+ data: {count, data},
275
+ isPending,
276
+ } = useQuery<{count: number; data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]}>({
277
+ ...options,
278
+ query: `{"count":${countQuery},"data":${dataQuery}}`,
279
+ params: {
280
+ ...params,
281
+ __handle: {
282
+ ...pick(instance.config, 'projectId', 'dataset', 'perspective'),
283
+ ...pick(options, 'projectId', 'dataset', 'perspective'),
284
+ },
285
+ __types: documentTypes,
286
+ },
287
+ })
288
+
289
+ // Now use the correctly typed variables
290
+ const hasMore = data.length < count
291
+
292
+ const loadMore = useCallback(() => {
293
+ setLimit((prev) => Math.min(prev + batchSize, count))
294
+ }, [count, batchSize])
295
+
296
+ return useMemo(
297
+ () => ({data, hasMore, count, isPending, loadMore}),
298
+ [count, data, hasMore, isPending, loadMore],
299
+ )
300
+ }
@@ -55,7 +55,7 @@ describe('createCallbackHook', () => {
55
55
  vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance1)
56
56
 
57
57
  // Create a test callback
58
- const testCallback = (instance: SanityInstance) => instance.identity.projectId
58
+ const testCallback = (instance: SanityInstance) => instance.config.projectId
59
59
 
60
60
  // Create and render our hook
61
61
  const useTestHook = createCallbackHook(testCallback)
@@ -87,7 +87,7 @@ describe('createCallbackHook', () => {
87
87
  method: string,
88
88
  data: object,
89
89
  ) => ({
90
- url: `${instance.identity.projectId}${path}`,
90
+ url: `${instance.config.projectId}${path}`,
91
91
  method,
92
92
  data,
93
93
  })
@@ -1,4 +1,4 @@
1
- import type {SanityInstance} from '@sanity/sdk'
1
+ import {type SanityInstance} from '@sanity/sdk'
2
2
  import {useCallback} from 'react'
3
3
 
4
4
  import {useSanityInstance} from '../context/useSanityInstance'
@@ -72,7 +72,7 @@ describe('createStateSourceHook', () => {
72
72
 
73
73
  const stateSourceFactory = vi.fn((instance: SanityInstance) => ({
74
74
  subscribe: vi.fn(),
75
- getCurrent: () => instance.identity.projectId,
75
+ getCurrent: () => instance.config.projectId,
76
76
  observable: throwError(() => new Error('unexpected usage of observable')),
77
77
  }))
78
78
 
@@ -127,4 +127,70 @@ describe('createStateSourceHook', () => {
127
127
  expect(result.current).toEqual('test_123')
128
128
  expect(stateSourceFactory).toHaveBeenCalledWith(mockInstance, 'test', 123)
129
129
  })
130
+
131
+ it('should throw suspender promise when shouldSuspend is true', () => {
132
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
133
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
134
+
135
+ const mockGetState = vi.fn().mockReturnValue({
136
+ subscribe: vi.fn(),
137
+ getCurrent: vi.fn().mockReturnValue('state'),
138
+ observable: throwError(() => new Error('unexpected usage of observable')),
139
+ })
140
+
141
+ const mockShouldSuspend = vi.fn().mockReturnValue(true)
142
+ const mockSuspender = vi.fn().mockReturnValue(Promise.resolve())
143
+
144
+ const options = {
145
+ getState: mockGetState,
146
+ shouldSuspend: mockShouldSuspend,
147
+ suspender: mockSuspender,
148
+ }
149
+
150
+ const useTestHook = createStateSourceHook(options)
151
+ const {result} = renderHook(() => {
152
+ try {
153
+ useTestHook('param1', 2)
154
+ } catch (e) {
155
+ return e
156
+ }
157
+ })
158
+
159
+ expect(mockShouldSuspend).toHaveBeenCalledWith(mockInstance, 'param1', 2)
160
+ expect(mockSuspender).toHaveBeenCalledWith(mockInstance, 'param1', 2)
161
+ expect(result.current).toBe(mockSuspender.mock.results[0].value)
162
+ expect(mockGetState).not.toHaveBeenCalled()
163
+ })
164
+
165
+ it('should not suspend when shouldSuspend returns false', () => {
166
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
167
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
168
+
169
+ const mockState = {value: 'test'}
170
+ const mockSubscribe = vi.fn()
171
+ const mockGetCurrent = vi.fn(() => mockState)
172
+
173
+ const mockGetState = vi.fn().mockReturnValue({
174
+ subscribe: mockSubscribe,
175
+ getCurrent: mockGetCurrent,
176
+ observable: throwError(() => new Error('unexpected usage of observable')),
177
+ })
178
+
179
+ const mockShouldSuspend = vi.fn().mockReturnValue(false)
180
+ const mockSuspender = vi.fn()
181
+
182
+ const options = {
183
+ getState: mockGetState,
184
+ shouldSuspend: mockShouldSuspend,
185
+ suspender: mockSuspender,
186
+ }
187
+
188
+ const useTestHook = createStateSourceHook(options)
189
+ const {result} = renderHook(() => useTestHook('param', 123))
190
+
191
+ expect(mockShouldSuspend).toHaveBeenCalledWith(mockInstance, 'param', 123)
192
+ expect(mockSuspender).not.toHaveBeenCalled()
193
+ expect(mockGetState).toHaveBeenCalledWith(mockInstance, 'param', 123)
194
+ expect(result.current).toBe(mockState)
195
+ })
130
196
  })
@@ -1,20 +1,36 @@
1
- import type {SanityInstance, StateSource} from '@sanity/sdk'
2
- import {useMemo, useSyncExternalStore} from 'react'
1
+ import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'
2
+ import {useSyncExternalStore} from 'react'
3
3
 
4
4
  import {useSanityInstance} from '../context/useSanityInstance'
5
5
 
6
+ type StateSourceFactory<TParams extends unknown[], TState> = (
7
+ instance: SanityInstance,
8
+ ...params: TParams
9
+ ) => StateSource<TState>
10
+
11
+ interface CreateStateSourceHookOptions<TParams extends unknown[], TState> {
12
+ getState: StateSourceFactory<TParams, TState>
13
+ shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean
14
+ suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>
15
+ getConfig?: (...params: TParams) => SanityConfig | undefined
16
+ }
17
+
6
18
  export function createStateSourceHook<TParams extends unknown[], TState>(
7
- stateSourceFactory: (instance: SanityInstance, ...params: TParams) => StateSource<TState>,
19
+ options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,
8
20
  ): (...params: TParams) => TState {
21
+ const getState = typeof options === 'function' ? options : options.getState
22
+ const getConfig = 'getConfig' in options ? options.getConfig : undefined
23
+ const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined
24
+
9
25
  function useHook(...params: TParams) {
10
- const instance = useSanityInstance()
11
- const {subscribe, getCurrent} = useMemo(
12
- () => stateSourceFactory(instance, ...params),
13
- // eslint-disable-next-line react-hooks/exhaustive-deps
14
- [instance, ...params],
15
- )
16
-
17
- return useSyncExternalStore(subscribe, getCurrent)
26
+ const instance = useSanityInstance(getConfig?.(...params))
27
+
28
+ if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {
29
+ throw suspense.suspender(instance, ...params)
30
+ }
31
+
32
+ const state = getState(instance, ...params)
33
+ return useSyncExternalStore(state.subscribe, state.getCurrent)
18
34
  }
19
35
 
20
36
  return useHook