@sanity/sdk-react 2.8.0 → 2.10.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 (53) hide show
  1. package/dist/index.d.ts +232 -47
  2. package/dist/index.js +468 -263
  3. package/dist/index.js.map +1 -1
  4. package/package.json +8 -10
  5. package/src/_exports/sdk-react.ts +5 -0
  6. package/src/components/SDKProvider.tsx +36 -8
  7. package/src/components/SanityApp.tsx +3 -2
  8. package/src/components/auth/AuthBoundary.tsx +8 -1
  9. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  10. package/src/components/auth/LoginError.test.tsx +191 -5
  11. package/src/components/auth/LoginError.tsx +100 -56
  12. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  13. package/src/components/errors/ChunkLoadError.tsx +56 -0
  14. package/src/components/errors/chunkReloadStorage.ts +57 -0
  15. package/src/context/ResourceProvider.test.tsx +7 -1
  16. package/src/context/ResourceProvider.tsx +11 -4
  17. package/src/context/ResourcesContext.tsx +7 -0
  18. package/src/context/SDKStudioContext.ts +6 -0
  19. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  20. package/src/context/SanityInstanceProvider.tsx +71 -0
  21. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  22. package/src/hooks/dashboard/useDispatchIntent.test.ts +8 -6
  23. package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
  24. package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
  25. package/src/hooks/dashboard/useWindowTitle.ts +112 -0
  26. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  27. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
  28. package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
  29. package/src/hooks/document/useApplyDocumentActions.ts +99 -3
  30. package/src/hooks/document/useDocument.ts +22 -6
  31. package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
  32. package/src/hooks/document/useDocumentEvent.ts +10 -3
  33. package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
  34. package/src/hooks/document/useDocumentPermissions.ts +22 -0
  35. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
  36. package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
  37. package/src/hooks/document/useEditDocument.ts +34 -8
  38. package/src/hooks/documents/useDocuments.ts +11 -6
  39. package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
  40. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  41. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +11 -8
  42. package/src/hooks/presence/usePresence.test.tsx +56 -9
  43. package/src/hooks/presence/usePresence.ts +25 -4
  44. package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
  45. package/src/hooks/preview/useDocumentPreview.tsx +40 -55
  46. package/src/hooks/projection/useDocumentProjection.ts +8 -6
  47. package/src/hooks/query/useQuery.ts +12 -9
  48. package/src/hooks/releases/useActiveReleases.ts +32 -13
  49. package/src/hooks/releases/usePerspective.ts +26 -14
  50. package/src/hooks/users/useUser.ts +2 -0
  51. package/src/hooks/users/useUsers.ts +2 -0
  52. package/src/context/SourcesContext.tsx +0 -7
  53. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
@@ -1,14 +1,25 @@
1
- import {type DocumentHandle, getPreviewState, type PreviewValue, resolvePreview} from '@sanity/sdk'
2
- import {useCallback, useSyncExternalStore} from 'react'
3
- import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
1
+ import {
2
+ type DocumentHandle,
3
+ PREVIEW_PROJECTION,
4
+ type PreviewQueryResult,
5
+ type PreviewValue,
6
+ transformProjectionToPreview,
7
+ } from '@sanity/sdk'
8
+ import {useMemo} from 'react'
4
9
 
5
10
  import {useSanityInstance} from '../context/useSanityInstance'
11
+ import {
12
+ useNormalizedResourceOptions,
13
+ type WithResourceNameSupport,
14
+ } from '../helpers/useNormalizedResourceOptions'
15
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
16
+ import {useDocumentProjection} from '../projection/useDocumentProjection'
6
17
 
7
18
  /**
8
19
  * @public
9
20
  * @category Types
10
21
  */
11
- export interface useDocumentPreviewOptions extends DocumentHandle {
22
+ export interface useDocumentPreviewOptions extends WithResourceNameSupport<DocumentHandle> {
12
23
  /**
13
24
  * Optional ref object to track visibility. When provided, preview resolution
14
25
  * only occurs when the referenced element is visible in the viewport.
@@ -21,7 +32,7 @@ export interface useDocumentPreviewOptions extends DocumentHandle {
21
32
  * @category Types
22
33
  */
23
34
  export interface useDocumentPreviewResults {
24
- /** The results of inferring the documents preview values */
35
+ /** The results of inferring the document's preview values */
25
36
  data: PreviewValue
26
37
  /** True when inferred preview values are being refreshed */
27
38
  isPending: boolean
@@ -31,7 +42,7 @@ export interface useDocumentPreviewResults {
31
42
  * @public
32
43
  *
33
44
  * Attempts to infer preview values of a document (specified via a `DocumentHandle`),
34
- * including the documents `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
45
+ * including the document's `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
35
46
  * To reduce unnecessary network requests for resolving the preview values, an optional `ref` can be passed to the hook so that preview
36
47
  * resolution will only occur if the `ref` is intersecting the current viewport.
37
48
  *
@@ -40,9 +51,13 @@ export interface useDocumentPreviewResults {
40
51
  * @remarks
41
52
  * Values returned by this hook may not be as expected. It is currently unable to read preview values as defined in your schema;
42
53
  * instead, it attempts to infer these preview values by checking against a basic set of potential fields on your document.
43
- * We are anticipating being able to significantly improve this hooks functionality and output in a future release.
54
+ * We are anticipating being able to significantly improve this hook's functionality and output in a future release.
44
55
  * For now, we recommend using {@link useDocumentProjection} for rendering individual document fields (or projections of those fields).
45
56
  *
57
+ * Internally, this hook is implemented as a specialized projection with post-processing logic.
58
+ * It uses a fixed GROQ projection to fetch common preview fields (title, subtitle, media) and
59
+ * transforms the results into the PreviewValue format.
60
+ *
46
61
  * @category Documents
47
62
  * @param options - The document handle for the document you want to infer preview values for, and an optional ref
48
63
  * @returns The inferred values for the given document and a boolean to indicate whether the resolution is pending
@@ -84,56 +99,26 @@ export function useDocumentPreview({
84
99
  ...docHandle
85
100
  }: useDocumentPreviewOptions): useDocumentPreviewResults {
86
101
  const instance = useSanityInstance(docHandle)
87
- const stateSource = getPreviewState(instance, docHandle)
88
-
89
- // Create subscribe function for useSyncExternalStore
90
- const subscribe = useCallback(
91
- (onStoreChanged: () => void) => {
92
- const subscription = new Observable<boolean>((observer) => {
93
- // For environments that don't have an intersection observer (e.g. server-side),
94
- // we pass true to always subscribe since we can't detect visibility
95
- if (typeof IntersectionObserver === 'undefined' || typeof HTMLElement === 'undefined') {
96
- observer.next(true)
97
- return
98
- }
102
+ trackHookUsage(instance, 'useDocumentPreview')
103
+ const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
99
104
 
100
- const intersectionObserver = new IntersectionObserver(
101
- ([entry]) => observer.next(entry.isIntersecting),
102
- {rootMargin: '0px', threshold: 0},
103
- )
104
- if (ref?.current && ref.current instanceof HTMLElement) {
105
- intersectionObserver.observe(ref.current)
106
- } else {
107
- // If no ref is provided or ref.current isn't an HTML element,
108
- // pass true to always subscribe since we can't track visibility
109
- observer.next(true)
110
- }
111
- return () => intersectionObserver.disconnect()
112
- })
113
- .pipe(
114
- startWith(false),
115
- distinctUntilChanged(),
116
- switchMap((isVisible) =>
117
- isVisible
118
- ? new Observable<void>((obs) => {
119
- return stateSource.subscribe(() => obs.next())
120
- })
121
- : EMPTY,
122
- ),
123
- )
124
- .subscribe({next: onStoreChanged})
105
+ // Use the projection hook with the fixed preview projection
106
+ const projectionResult = useDocumentProjection<PreviewQueryResult>({
107
+ ...normalizedDocHandle,
108
+ projection: PREVIEW_PROJECTION,
109
+ ref,
110
+ })
125
111
 
126
- return () => subscription.unsubscribe()
127
- },
128
- [stateSource, ref],
112
+ // Contract: useDocumentProjection suspends while data is null, so data is always available here.
113
+ // Keep this non-null assumption aligned with useDocumentPreviewResults.data.
114
+ const previewValue = useMemo(
115
+ () =>
116
+ transformProjectionToPreview(instance, projectionResult.data, normalizedDocHandle.resource),
117
+ [projectionResult.data, instance, normalizedDocHandle.resource],
129
118
  )
130
119
 
131
- // Create getSnapshot function to return current state
132
- const getSnapshot = useCallback(() => {
133
- const currentState = stateSource.getCurrent()
134
- if (currentState.data === null) throw resolvePreview(instance, docHandle)
135
- return currentState as useDocumentPreviewResults
136
- }, [docHandle, instance, stateSource])
137
-
138
- return useSyncExternalStore(subscribe, getSnapshot)
120
+ return {
121
+ data: previewValue,
122
+ isPending: projectionResult.isPending,
123
+ }
139
124
  }
@@ -5,9 +5,10 @@ import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxj
5
5
 
6
6
  import {useSanityInstance} from '../context/useSanityInstance'
7
7
  import {
8
- useNormalizedSourceOptions,
9
- type WithSourceNameSupport,
10
- } from '../helpers/useNormalizedSourceOptions'
8
+ useNormalizedResourceOptions,
9
+ type WithResourceNameSupport,
10
+ } from '../helpers/useNormalizedResourceOptions'
11
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
11
12
 
12
13
  /**
13
14
  * @public
@@ -18,7 +19,7 @@ export interface useDocumentProjectionOptions<
18
19
  TDocumentType extends string = string,
19
20
  TDataset extends string = string,
20
21
  TProjectId extends string = string,
21
- > extends WithSourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
22
+ > extends WithResourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
22
23
  /** The GROQ projection string */
23
24
  projection: TProjection
24
25
  /** Optional parameters for the projection query */
@@ -181,14 +182,15 @@ export function useDocumentProjection<TData extends object>({
181
182
  ...docHandle
182
183
  }: useDocumentProjectionOptions): useDocumentProjectionResults<TData> {
183
184
  const instance = useSanityInstance(docHandle)
185
+ trackHookUsage(instance, 'useDocumentProjection')
184
186
 
185
187
  // Normalize projection string to handle template literals with whitespace
186
188
  // This ensures that the same projection content produces the same state source
187
189
  // even if the string reference changes (e.g., from inline template literals)
188
190
  const normalizedProjection = useMemo(() => projection.trim(), [projection])
189
191
 
190
- // Normalize options: resolve sourceName to source and strip sourceName
191
- const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
192
+ // Normalize options: resolve resourceName to resource and strip resourceName
193
+ const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
192
194
 
193
195
  // Memoize stateSource based on normalized projection and docHandle properties
194
196
  // This prevents creating a new StateSource on every render when projection content is the same
@@ -10,18 +10,19 @@ import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransitio
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
12
  import {
13
- useNormalizedSourceOptions,
14
- type WithSourceNameSupport,
15
- } from '../helpers/useNormalizedSourceOptions'
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
16
17
  /**
17
- * Hook options for useQuery, supporting both direct source and sourceName.
18
+ * Hook options for useQuery, supporting both direct resource and resourceName.
18
19
  * @beta
19
20
  */
20
21
  type UseQueryOptions<
21
22
  TQuery extends string = string,
22
23
  TDataset extends string = string,
23
24
  TProjectId extends string = string,
24
- > = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
25
+ > = WithResourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
25
26
 
26
27
  // Overload 1: Inferred Type (using Typegen)
27
28
  /**
@@ -121,7 +122,7 @@ export function useQuery<
121
122
  * }
122
123
  * ```
123
124
  */
124
- export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
125
+ export function useQuery<TData>(options: WithResourceNameSupport<QueryOptions>): {
125
126
  /** The query result, cast to the provided type TData */
126
127
  data: TData
127
128
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -146,15 +147,16 @@ export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
146
147
  *
147
148
  * @category GROQ
148
149
  */
149
- export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
150
+ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
150
151
  data: unknown
151
152
  isPending: boolean
152
153
  } {
153
154
  // Implementation returns unknown, overloads define specifics
154
155
  const instance = useSanityInstance(options)
156
+ trackHookUsage(instance, 'useQuery')
155
157
 
156
- // Normalize options: resolve sourceName to source and strip sourceName
157
- const normalized = useNormalizedSourceOptions(options)
158
+ // Normalize options: resolve resourceName to resource and strip resourceName
159
+ const normalized = useNormalizedResourceOptions(options)
158
160
 
159
161
  // Use React's useTransition to avoid UI jank when queries change
160
162
  const [isPending, startTransition] = useTransition()
@@ -202,6 +204,7 @@ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
202
204
  const currentSignal = ref.current.signal
203
205
  const deferred = parseQueryKey(deferredQueryKey)
204
206
 
207
+ // eslint-disable-next-line react-hooks/refs -- intentional during suspended render; see comment above.
205
208
  throw resolveQuery(instance, {...deferred, signal: currentSignal})
206
209
  }
207
210
 
@@ -1,19 +1,18 @@
1
1
  import {
2
+ type DocumentResource,
2
3
  getActiveReleasesState,
3
4
  type ReleaseDocument,
5
+ type SanityConfig,
4
6
  type SanityInstance,
5
7
  type StateSource,
6
8
  } from '@sanity/sdk'
7
9
  import {filter, firstValueFrom} from 'rxjs'
8
10
 
9
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
10
-
11
- /**
12
- * @public
13
- */
14
- type UseActiveReleases = {
15
- (): ReleaseDocument[]
16
- }
12
+ import {
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
17
16
 
18
17
  /**
19
18
  * @public
@@ -30,10 +29,30 @@ type UseActiveReleases = {
30
29
  * const activeReleases = useActiveReleases()
31
30
  * ```
32
31
  */
33
- export const useActiveReleases: UseActiveReleases = createStateSourceHook({
34
- getState: getActiveReleasesState as (instance: SanityInstance) => StateSource<ReleaseDocument[]>,
35
- shouldSuspend: (instance: SanityInstance) =>
36
- getActiveReleasesState(instance).getCurrent() === undefined,
37
- suspender: (instance: SanityInstance) =>
38
- firstValueFrom(getActiveReleasesState(instance).observable.pipe(filter(Boolean))),
32
+ type UseActiveReleases = {
33
+ (options?: WithResourceNameSupport<SanityConfig> | undefined): ReleaseDocument[]
34
+ }
35
+
36
+ const useActiveReleasesValue: UseActiveReleases = createStateSourceHook({
37
+ getState: getActiveReleasesState as (
38
+ instance: SanityInstance,
39
+ options?: {resource?: DocumentResource},
40
+ ) => StateSource<ReleaseDocument[]>,
41
+ shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
42
+ getActiveReleasesState(instance, options ?? {}).getCurrent() === undefined,
43
+ suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
44
+ firstValueFrom(
45
+ getActiveReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean)),
46
+ ),
39
47
  })
48
+
49
+ /**
50
+ * @public
51
+ * @function
52
+ */
53
+ export const useActiveReleases: UseActiveReleases = (
54
+ options: WithResourceNameSupport<{resource?: DocumentResource}> | undefined,
55
+ ) => {
56
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
57
+ return useActiveReleasesValue(normalizedOptions)
58
+ }
@@ -1,20 +1,17 @@
1
1
  import {
2
- getActiveReleasesState,
2
+ type DatasetHandle,
3
+ type DocumentResource,
3
4
  getPerspectiveState,
4
- type PerspectiveHandle,
5
5
  type SanityInstance,
6
6
  type StateSource,
7
7
  } from '@sanity/sdk'
8
8
  import {filter, firstValueFrom} from 'rxjs'
9
9
 
10
10
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
11
-
12
- /**
13
- * @public
14
- */
15
- type UsePerspective = {
16
- (perspectiveHandle: PerspectiveHandle): string | string[]
17
- }
11
+ import {
12
+ useNormalizedResourceOptions,
13
+ type WithResourceNameSupport,
14
+ } from '../helpers/useNormalizedResourceOptions'
18
15
 
19
16
  /**
20
17
  * @public
@@ -38,13 +35,28 @@ type UsePerspective = {
38
35
  *
39
36
  * @returns The perspective for the given perspective handle.
40
37
  */
41
- export const usePerspective: UsePerspective = createStateSourceHook({
38
+ type UsePerspective = {
39
+ (perspectiveHandle: DatasetHandle): string | string[]
40
+ }
41
+
42
+ const usePerspectiveValue: UsePerspective = createStateSourceHook({
42
43
  getState: getPerspectiveState as (
43
44
  instance: SanityInstance,
44
- perspectiveHandle?: PerspectiveHandle,
45
+ perspectiveHandle?: {resource?: DocumentResource},
45
46
  ) => StateSource<string | string[]>,
46
- shouldSuspend: (instance: SanityInstance, options: PerspectiveHandle): boolean =>
47
+ shouldSuspend: (instance: SanityInstance, options: {resource?: DocumentResource}): boolean =>
47
48
  getPerspectiveState(instance, options).getCurrent() === undefined,
48
- suspender: (instance: SanityInstance, _options?: PerspectiveHandle) =>
49
- firstValueFrom(getActiveReleasesState(instance).observable.pipe(filter(Boolean))),
49
+ suspender: (instance: SanityInstance, _options?: {resource?: DocumentResource}) =>
50
+ firstValueFrom(getPerspectiveState(instance, _options ?? {}).observable.pipe(filter(Boolean))),
50
51
  })
52
+
53
+ /**
54
+ * @public
55
+ * @function
56
+ */
57
+ export const usePerspective: UsePerspective = (
58
+ options: WithResourceNameSupport<DatasetHandle> | undefined,
59
+ ) => {
60
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
61
+ return usePerspectiveValue(normalizedOptions)
62
+ }
@@ -9,6 +9,7 @@ import {
9
9
  import {useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
12
13
 
13
14
  /**
14
15
  * @public
@@ -59,6 +60,7 @@ export interface UserResult {
59
60
  */
60
61
  export function useUser(options: GetUserOptions): UserResult {
61
62
  const instance = useSanityInstance(options)
63
+ trackHookUsage(instance, 'useUser')
62
64
  // Use React's useTransition to avoid UI jank when user options change
63
65
  const [isPending, startTransition] = useTransition()
64
66
 
@@ -10,6 +10,7 @@ import {
10
10
  import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
11
11
 
12
12
  import {useSanityInstance} from '../context/useSanityInstance'
13
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
13
14
 
14
15
  /**
15
16
  * @public
@@ -69,6 +70,7 @@ export interface UsersResult {
69
70
  */
70
71
  export function useUsers(options?: GetUsersOptions): UsersResult {
71
72
  const instance = useSanityInstance(options)
73
+ trackHookUsage(instance, 'useUsers')
72
74
  // Use React's useTransition to avoid UI jank when user options change
73
75
  const [isPending, startTransition] = useTransition()
74
76
 
@@ -1,7 +0,0 @@
1
- import {type DocumentSource} from '@sanity/sdk'
2
- import {createContext} from 'react'
3
-
4
- /** Context for sources.
5
- * @beta
6
- */
7
- export const SourcesContext = createContext<Record<string, DocumentSource>>({})
@@ -1,85 +0,0 @@
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
- }