@sanity/sdk-react 2.6.0 → 2.8.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.
@@ -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
+ }
@@ -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,16 +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 {useSource} from '../context/useSource'
13
-
14
- interface UseQueryOptions<
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<
15
21
  TQuery extends string = string,
16
22
  TDataset extends string = string,
17
23
  TProjectId extends string = string,
18
- TSourceName extends string = string,
19
- > extends QueryOptions<TQuery, TDataset, TProjectId> {
20
- sourceName?: TSourceName
21
- }
24
+ > = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
22
25
 
23
26
  // Overload 1: Inferred Type (using Typegen)
24
27
  /**
@@ -80,9 +83,8 @@ export function useQuery<
80
83
  TQuery extends string = string,
81
84
  TDataset extends string = string,
82
85
  TProjectId extends string = string,
83
- TSourceName extends string = string,
84
86
  >(
85
- options: UseQueryOptions<TQuery, TDataset, TProjectId, TSourceName>,
87
+ options: UseQueryOptions<TQuery, TDataset, TProjectId>,
86
88
  ): {
87
89
  /** The query result, typed based on the GROQ query string */
88
90
  data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
@@ -119,7 +121,7 @@ export function useQuery<
119
121
  * }
120
122
  * ```
121
123
  */
122
- export function useQuery<TData>(options: QueryOptions): {
124
+ export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
123
125
  /** The query result, cast to the provided type TData */
124
126
  data: TData
125
127
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -144,17 +146,21 @@ export function useQuery<TData>(options: QueryOptions): {
144
146
  *
145
147
  * @category GROQ
146
148
  */
147
- export function useQuery(options: QueryOptions): {data: unknown; isPending: boolean} {
149
+ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
150
+ data: unknown
151
+ isPending: boolean
152
+ } {
148
153
  // Implementation returns unknown, overloads define specifics
149
154
  const instance = useSanityInstance(options)
150
155
 
151
- const source = useSource(options)
156
+ // Normalize options: resolve sourceName to source and strip sourceName
157
+ const normalized = useNormalizedSourceOptions(options)
152
158
 
153
159
  // Use React's useTransition to avoid UI jank when queries change
154
160
  const [isPending, startTransition] = useTransition()
155
161
 
156
- // Get the unique key for this query and its options
157
- const queryKey = getQueryKey(options)
162
+ // Get the unique key for this query and its options (using normalized options)
163
+ const queryKey = getQueryKey(normalized)
158
164
  // Use a deferred state to avoid immediate re-renders when the query changes
159
165
  const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
160
166
 
@@ -180,8 +186,8 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
180
186
  // Memoize the options object by depending on the stable string key instead of the parsed object
181
187
  const {getCurrent, subscribe} = useMemo(() => {
182
188
  const deferred = parseQueryKey(deferredQueryKey)
183
- return getQueryState(instance, {...deferred, source})
184
- }, [instance, deferredQueryKey, source])
189
+ return getQueryState(instance, deferred)
190
+ }, [instance, deferredQueryKey])
185
191
 
186
192
  // If data isn't available yet, suspend rendering
187
193
  if (getCurrent() === undefined) {
@@ -196,7 +202,7 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
196
202
  const currentSignal = ref.current.signal
197
203
  const deferred = parseQueryKey(deferredQueryKey)
198
204
 
199
- throw resolveQuery(instance, {...deferred, source, signal: currentSignal})
205
+ throw resolveQuery(instance, {...deferred, signal: currentSignal})
200
206
  }
201
207
 
202
208
  // Subscribe to updates and get the current data
@@ -1,34 +0,0 @@
1
- import {type DatasetHandle, type DocumentHandle, type DocumentSource} from '@sanity/sdk'
2
- import {useContext} from 'react'
3
-
4
- import {SourcesContext} from '../../context/SourcesContext'
5
-
6
- /** Retrieves the named source from context.
7
- * @beta
8
- * @param name - The name of the source to retrieve.
9
- * @returns The source.
10
- * @example
11
- * ```tsx
12
- * const source = useSource('my-source')
13
- * ```
14
- */
15
- export function useSource(options: DocumentHandle | DatasetHandle): DocumentSource | undefined {
16
- const sources = useContext(SourcesContext)
17
-
18
- // this might return the "default" source in the future once we implement it?
19
- if (!options.sourceName && !options.source) {
20
- return undefined
21
- }
22
-
23
- if (options.source) {
24
- return options.source
25
- }
26
-
27
- if (options.sourceName && !Object.hasOwn(sources, options.sourceName)) {
28
- throw new Error(
29
- `There's no source named ${JSON.stringify(options.sourceName)} in context. Please use <SourceProvider>.`,
30
- )
31
- }
32
-
33
- return options.sourceName ? sources[options.sourceName] : undefined
34
- }