@sanity/sdk-react 2.7.0 → 3.0.0-rc.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 (88) hide show
  1. package/README.md +125 -63
  2. package/dist/index.d.ts +381 -571
  3. package/dist/index.js +450 -366
  4. package/dist/index.js.map +1 -1
  5. package/package.json +6 -8
  6. package/src/_exports/index.ts +4 -0
  7. package/src/_exports/sdk-react.ts +16 -0
  8. package/src/components/SDKProvider.test.tsx +23 -58
  9. package/src/components/SDKProvider.tsx +38 -30
  10. package/src/components/SanityApp.test.tsx +12 -68
  11. package/src/components/SanityApp.tsx +88 -65
  12. package/src/components/auth/AuthBoundary.test.tsx +11 -26
  13. package/src/components/auth/LoginError.test.tsx +5 -0
  14. package/src/components/auth/LoginError.tsx +23 -2
  15. package/src/config/handles.ts +53 -0
  16. package/src/context/ComlinkTokenRefresh.test.tsx +27 -10
  17. package/src/context/DefaultResourceContext.ts +10 -0
  18. package/src/context/PerspectiveContext.ts +12 -0
  19. package/src/context/ResourceProvider.test.tsx +99 -19
  20. package/src/context/ResourceProvider.tsx +103 -37
  21. package/src/context/ResourcesContext.tsx +7 -0
  22. package/src/context/SDKStudioContext.test.tsx +33 -28
  23. package/src/context/SDKStudioContext.ts +6 -0
  24. package/src/context/renderSanityApp.test.tsx +49 -151
  25. package/src/context/renderSanityApp.tsx +8 -12
  26. package/src/hooks/agent/agentActions.test.tsx +1 -1
  27. package/src/hooks/agent/agentActions.ts +56 -19
  28. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +8 -2
  29. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +32 -8
  30. package/src/hooks/client/useClient.test.tsx +4 -1
  31. package/src/hooks/client/useClient.ts +0 -1
  32. package/src/hooks/context/useDefaultResource.test.tsx +25 -0
  33. package/src/hooks/context/useDefaultResource.ts +30 -0
  34. package/src/hooks/context/useSanityInstance.test.tsx +2 -140
  35. package/src/hooks/context/useSanityInstance.ts +9 -53
  36. package/src/hooks/dashboard/useDispatchIntent.test.ts +24 -15
  37. package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
  38. package/src/hooks/dashboard/useManageFavorite.test.tsx +34 -94
  39. package/src/hooks/dashboard/useManageFavorite.ts +16 -10
  40. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +7 -5
  41. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +6 -2
  42. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +2 -0
  43. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
  44. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +17 -38
  45. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +12 -19
  46. package/src/hooks/datasets/useDatasets.test.ts +8 -22
  47. package/src/hooks/datasets/useDatasets.ts +8 -16
  48. package/src/hooks/document/useApplyDocumentActions.test.ts +98 -52
  49. package/src/hooks/document/useApplyDocumentActions.ts +35 -37
  50. package/src/hooks/document/useDocument.test.tsx +8 -37
  51. package/src/hooks/document/useDocument.ts +78 -129
  52. package/src/hooks/document/useDocumentEvent.test.tsx +7 -19
  53. package/src/hooks/document/useDocumentEvent.ts +21 -19
  54. package/src/hooks/document/useDocumentPermissions.test.tsx +75 -84
  55. package/src/hooks/document/useDocumentPermissions.ts +41 -28
  56. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -3
  57. package/src/hooks/document/useDocumentSyncStatus.ts +19 -14
  58. package/src/hooks/document/useEditDocument.test.tsx +28 -70
  59. package/src/hooks/document/useEditDocument.ts +29 -149
  60. package/src/hooks/documents/useDocuments.test.tsx +44 -64
  61. package/src/hooks/documents/useDocuments.ts +19 -25
  62. package/src/hooks/helpers/createCallbackHook.test.tsx +19 -13
  63. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  64. package/src/hooks/helpers/createStateSourceHook.tsx +2 -4
  65. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +65 -0
  66. package/src/hooks/helpers/useNormalizedResourceOptions.ts +127 -0
  67. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +27 -34
  68. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +19 -20
  69. package/src/hooks/presence/usePresence.test.tsx +71 -9
  70. package/src/hooks/presence/usePresence.ts +28 -3
  71. package/src/hooks/preview/useDocumentPreview.test.tsx +85 -193
  72. package/src/hooks/preview/useDocumentPreview.tsx +42 -62
  73. package/src/hooks/projection/useDocumentProjection.test.tsx +9 -37
  74. package/src/hooks/projection/useDocumentProjection.ts +9 -82
  75. package/src/hooks/projects/useProject.test.ts +1 -2
  76. package/src/hooks/projects/useProject.ts +7 -8
  77. package/src/hooks/query/useQuery.test.tsx +5 -6
  78. package/src/hooks/query/useQuery.ts +12 -91
  79. package/src/hooks/releases/useActiveReleases.test.tsx +2 -2
  80. package/src/hooks/releases/useActiveReleases.ts +25 -13
  81. package/src/hooks/releases/usePerspective.test.tsx +9 -17
  82. package/src/hooks/releases/usePerspective.ts +29 -18
  83. package/src/hooks/users/useUser.test.tsx +9 -3
  84. package/src/hooks/users/useUser.ts +1 -1
  85. package/src/hooks/users/useUsers.test.tsx +5 -2
  86. package/src/hooks/users/useUsers.ts +1 -1
  87. package/src/context/SourcesContext.tsx +0 -7
  88. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
@@ -1,13 +1,10 @@
1
- import {type DocumentHandle, getProjectionState, resolveProjection} from '@sanity/sdk'
2
- import {type SanityProjectionResult} from 'groq'
1
+ import {getProjectionState, resolveProjection} from '@sanity/sdk'
3
2
  import {useCallback, useMemo, useSyncExternalStore} from 'react'
4
3
  import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
5
4
 
5
+ import {type DocumentHandle} from '../../config/handles'
6
6
  import {useSanityInstance} from '../context/useSanityInstance'
7
- import {
8
- useNormalizedSourceOptions,
9
- type WithSourceNameSupport,
10
- } from '../helpers/useNormalizedSourceOptions'
7
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
11
8
 
12
9
  /**
13
10
  * @public
@@ -18,7 +15,7 @@ export interface useDocumentProjectionOptions<
18
15
  TDocumentType extends string = string,
19
16
  TDataset extends string = string,
20
17
  TProjectId extends string = string,
21
- > extends WithSourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
18
+ > extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
22
19
  /** The GROQ projection string */
23
20
  projection: TProjection
24
21
  /** Optional parameters for the projection query */
@@ -48,83 +45,13 @@ export interface useDocumentProjectionResults<TData> {
48
45
  *
49
46
  * @category Documents
50
47
  * @remarks
51
- * This hook has multiple signatures allowing for fine-grained control over type inference:
52
- * - Using Typegen: Infers the return type based on the `documentType`, `dataset`, `projectId`, and `projection`.
53
- * - Using explicit type parameter: Allows specifying a custom return type `TData`.
48
+ * This hook allows specifying an explicit type parameter `TData` for the projected result.
54
49
  *
55
50
  * @param options - An object containing the `DocumentHandle` properties (`documentId`, `documentType`, etc.), the `projection` string, optional `params`, and an optional `ref`.
56
51
  * @returns An object containing the projection results (`data`) and a boolean indicating whether the resolution is pending (`isPending`). Note: Suspense handles initial loading states; `data` being `undefined` after initial loading means the document doesn't exist or the projection yielded no result.
57
52
  */
58
53
 
59
- // Overload 1: Relies on Typegen
60
- /**
61
- * @public
62
- * Fetch a projection, relying on Typegen for the return type based on the handle and projection.
63
- *
64
- * @category Documents
65
- * @param options - Options including the document handle properties (`documentId`, `documentType`, etc.) and the `projection`.
66
- * @returns The projected data, typed based on Typegen.
67
- *
68
- * @example Using Typegen for a book preview
69
- * ```tsx
70
- * // ProjectionComponent.tsx
71
- * import {useDocumentProjection, type DocumentHandle} from '@sanity/sdk-react'
72
- * import {useRef} from 'react'
73
- * import {defineProjection} from 'groq'
74
- *
75
- * // Define props using DocumentHandle with the specific document type
76
- * type ProjectionComponentProps = {
77
- * doc: DocumentHandle<'book'> // Typegen knows 'book'
78
- * }
79
- *
80
- * // This is required for typegen to generate the correct return type
81
- * const myProjection = defineProjection(`{
82
- * title,
83
- * 'coverImage': cover.asset->url,
84
- * 'authors': array::join(authors[]->{'name': firstName + ' ' + lastName}.name, ', ')
85
- * }`)
86
- *
87
- * export default function ProjectionComponent({ doc }: ProjectionComponentProps) {
88
- * const ref = useRef(null) // Optional ref to track viewport intersection for lazy loading
89
- *
90
- * // Spread the doc handle into the options
91
- * // Typegen infers the return type based on 'book' and the projection
92
- * const { data } = useDocumentProjection({
93
- * ...doc, // Pass the handle properties
94
- * ref,
95
- * projection: myProjection,
96
- * })
97
- *
98
- * // Suspense handles initial load, check for data existence after
99
- * return (
100
- * <article ref={ref}>
101
- * <h2>{data.title ?? 'Untitled'}</h2>
102
- * {data.coverImage && <img src={data.coverImage} alt={data.title} />}
103
- * <p>{data.authors ?? 'Unknown authors'}</p>
104
- * </article>
105
- * )
106
- * }
107
- *
108
- * // Usage:
109
- * // import {createDocumentHandle} from '@sanity/sdk-react'
110
- * // const myDocHandle = createDocumentHandle({ documentId: 'book123', documentType: 'book' })
111
- * // <Suspense fallback='Loading preview...'>
112
- * // <ProjectionComponent doc={myDocHandle} />
113
- * // </Suspense>
114
- * ```
115
- */
116
- export function useDocumentProjection<
117
- TProjection extends string = string,
118
- TDocumentType extends string = string,
119
- TDataset extends string = string,
120
- TProjectId extends string = string,
121
- >(
122
- options: useDocumentProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
123
- ): useDocumentProjectionResults<
124
- SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
125
- >
126
-
127
- // Overload 2: Explicit type provided
54
+ // Overload 1: Explicit type provided
128
55
  /**
129
56
  * @public
130
57
  * Fetch a projection with an explicitly defined return type `TData`.
@@ -180,15 +107,15 @@ export function useDocumentProjection<TData extends object>({
180
107
  projection,
181
108
  ...docHandle
182
109
  }: useDocumentProjectionOptions): useDocumentProjectionResults<TData> {
183
- const instance = useSanityInstance(docHandle)
110
+ const instance = useSanityInstance()
184
111
 
185
112
  // Normalize projection string to handle template literals with whitespace
186
113
  // This ensures that the same projection content produces the same state source
187
114
  // even if the string reference changes (e.g., from inline template literals)
188
115
  const normalizedProjection = useMemo(() => projection.trim(), [projection])
189
116
 
190
- // Normalize options: resolve sourceName to source and strip sourceName
191
- const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
117
+ // Normalize options: resolve resourceName to resource and strip resourceName
118
+ const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
192
119
 
193
120
  // Memoize stateSource based on normalized projection and docHandle properties
194
121
  // This prevents creating a new StateSource on every render when projection content is the same
@@ -40,8 +40,7 @@ describe('useProject', () => {
40
40
  expect.objectContaining({
41
41
  getState: expect.any(Function),
42
42
  shouldSuspend: expect.any(Function),
43
- suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
44
- getConfig: expect.any(Function), // Actual function reference doesn't matter here
43
+ suspender: expect.any(Function),
45
44
  }),
46
45
  )
47
46
  })
@@ -6,7 +6,6 @@ import {
6
6
  type SanityProject,
7
7
  type StateSource,
8
8
  } from '@sanity/sdk'
9
- import {identity} from 'rxjs'
10
9
 
11
10
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
11
 
@@ -16,12 +15,12 @@ type UseProject = {
16
15
  * Returns metadata for a given project
17
16
  *
18
17
  * @category Projects
19
- * @param projectId - The ID of the project to retrieve metadata for
18
+ * @param projectHandle - An optional project handle identifying which project to retrieve metadata for
20
19
  * @returns The metadata for the project
21
20
  * @example
22
21
  * ```tsx
23
22
  * function ProjectMetadata({ projectId }: { projectId: string }) {
24
- * const project = useProject(projectId)
23
+ * const project = useProject({ projectId })
25
24
  *
26
25
  * return (
27
26
  * <figure style={{ backgroundColor: project.metadata.color || 'lavender'}}>
@@ -44,8 +43,8 @@ export const useProject: UseProject = createStateSourceHook({
44
43
  instance: SanityInstance,
45
44
  projectHandle?: ProjectHandle,
46
45
  ) => StateSource<SanityProject>,
47
- shouldSuspend: (instance, projectHandle) =>
48
- getProjectState(instance, projectHandle).getCurrent() === undefined,
49
- suspender: resolveProject,
50
- getConfig: identity,
51
- })
46
+ shouldSuspend: (instance: SanityInstance, projectHandle?: ProjectHandle) =>
47
+ getProjectState(instance, projectHandle as ProjectHandle).getCurrent() === undefined,
48
+ suspender: (instance: SanityInstance, projectHandle?: ProjectHandle) =>
49
+ resolveProject(instance, projectHandle as ProjectHandle),
50
+ }) as UseProject
@@ -33,7 +33,7 @@ describe('useQuery', () => {
33
33
  } as StateSource<unknown>)
34
34
 
35
35
  function TestComponent() {
36
- const {data, isPending} = useQuery({query: 'test query'})
36
+ const {data, isPending} = useQuery<string>({query: 'test query'})
37
37
  return (
38
38
  <div data-testid="output">
39
39
  {data} - {isPending ? 'pending' : 'not pending'}
@@ -42,7 +42,7 @@ describe('useQuery', () => {
42
42
  }
43
43
 
44
44
  render(
45
- <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
45
+ <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
46
46
  <TestComponent />
47
47
  </ResourceProvider>,
48
48
  )
@@ -82,14 +82,13 @@ describe('useQuery', () => {
82
82
  )
83
83
 
84
84
  function TestComponent() {
85
- const {data} = useQuery({query: 'test query'})
85
+ const {data} = useQuery<string>({query: 'test query'})
86
86
  return <div data-testid="output">{data}</div>
87
87
  }
88
88
 
89
89
  render(
90
90
  <ResourceProvider
91
- projectId="p"
92
- dataset="d"
91
+ resource={{projectId: 'p', dataset: 'd'}}
93
92
  fallback={<div data-testid="fallback">Loading...</div>}
94
93
  >
95
94
  <TestComponent />
@@ -164,7 +163,7 @@ describe('useQuery', () => {
164
163
  }
165
164
 
166
165
  render(
167
- <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
166
+ <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
168
167
  <WrapperComponent />
169
168
  </ResourceProvider>,
170
169
  )
@@ -5,99 +5,21 @@ import {
5
5
  type QueryOptions,
6
6
  resolveQuery,
7
7
  } from '@sanity/sdk'
8
- import {type SanityQueryResult} from 'groq'
9
8
  import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
10
9
 
10
+ import {type ResourceHandle} from '../../config/handles'
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
- import {
13
- useNormalizedSourceOptions,
14
- type WithSourceNameSupport,
15
- } from '../helpers/useNormalizedSourceOptions'
16
- /**
17
- * Hook options for useQuery, supporting both direct source and sourceName.
18
- * @beta
19
- */
20
- type UseQueryOptions<
21
- TQuery extends string = string,
22
- TDataset extends string = string,
23
- TProjectId extends string = string,
24
- > = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
12
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
25
13
 
26
- // Overload 1: Inferred Type (using Typegen)
27
- /**
28
- * @public
29
- * Executes a GROQ query, inferring the result type from the query string and options.
30
- * Leverages Sanity Typegen if configured for enhanced type safety.
31
- *
32
- * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
33
- * @returns An object containing `data` (typed based on the query) and `isPending` (for transitions).
34
- *
35
- * @example Basic usage (Inferred Type)
36
- * ```tsx
37
- * import {useQuery} from '@sanity/sdk-react'
38
- * import {defineQuery} from 'groq'
39
- *
40
- * const myQuery = defineQuery(`*[_type == "movie"]{_id, title}`)
41
- *
42
- * function MovieList() {
43
- * // Typegen infers the return type for data
44
- * const {data} = useQuery({ query: myQuery })
45
- *
46
- * return (
47
- * <div>
48
- * <h2>Movies</h2>
49
- * <ul>
50
- * {data.map(movie => <li key={movie._id}>{movie.title}</li>)}
51
- * </ul>
52
- * </div>
53
- * )
54
- * }
55
- * // Suspense boundary should wrap <MovieList /> for initial load
56
- * ```
57
- *
58
- * @example Using parameters (Inferred Type)
59
- * ```tsx
60
- * import {useQuery} from '@sanity/sdk-react'
61
- * import {defineQuery} from 'groq'
62
- *
63
- * const myQuery = defineQuery(`*[_type == "movie" && _id == $id][0]`)
64
- *
65
- * function MovieDetails({movieId}: {movieId: string}) {
66
- * // Typegen infers the return type based on query and params
67
- * const {data, isPending} = useQuery({
68
- * query: myQuery,
69
- * params: { id: movieId }
70
- * })
71
- *
72
- * return (
73
- * // utilize `isPending` to signal to users that new data is coming in
74
- * // (e.g. the `movieId` changed and we're loading in the new one)
75
- * <div style={{ opacity: isPending ? 0.5 : 1 }}>
76
- * {data ? <h1>{data.title}</h1> : <p>Movie not found</p>}
77
- * </div>
78
- * )
79
- * }
80
- * ```
81
- */
82
- export function useQuery<
83
- TQuery extends string = string,
84
- TDataset extends string = string,
85
- TProjectId extends string = string,
86
- >(
87
- options: UseQueryOptions<TQuery, TDataset, TProjectId>,
88
- ): {
89
- /** The query result, typed based on the GROQ query string */
90
- data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
91
- /** True if a query transition is in progress */
92
- isPending: boolean
93
- }
14
+ /** Options for useQuery: QueryOptions with resource made optional (resolved from context) */
15
+ type ReactQueryOptions = Omit<QueryOptions, 'resource' | 'resourceName'> & ResourceHandle
94
16
 
95
- // Overload 2: Explicit Type Provided
17
+ // Overload 1: Explicit Type Provided
96
18
  /**
97
19
  * @public
98
20
  * Executes a GROQ query with an explicitly provided result type `TData`.
99
21
  *
100
- * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
22
+ * @param options - Configuration for the query, including `query`, optional `params`, `resource`, etc.
101
23
  * @returns An object containing `data` (cast to `TData`) and `isPending` (indicates whether a query resolution is pending; note that Suspense handles initial loading states). *
102
24
  * @example Manually typed query result
103
25
  * ```tsx
@@ -121,7 +43,7 @@ export function useQuery<
121
43
  * }
122
44
  * ```
123
45
  */
124
- export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
46
+ export function useQuery<TData>(options: ReactQueryOptions): {
125
47
  /** The query result, cast to the provided type TData */
126
48
  data: TData
127
49
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -141,20 +63,19 @@ export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
141
63
  * - Subscribes to changes, providing real-time updates.
142
64
  * - Integrates with React Suspense for handling initial loading states.
143
65
  * - Uses React Transitions for managing loading states during query/parameter changes (indicated by `isPending`).
144
- * - Supports type inference based on the GROQ query when using Sanity Typegen.
145
66
  * - Allows specifying an explicit return type `TData` for the query result.
146
67
  *
147
68
  * @category GROQ
148
69
  */
149
- export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
70
+ export function useQuery(options: ReactQueryOptions): {
150
71
  data: unknown
151
72
  isPending: boolean
152
73
  } {
153
74
  // Implementation returns unknown, overloads define specifics
154
- const instance = useSanityInstance(options)
75
+ const instance = useSanityInstance()
155
76
 
156
- // Normalize options: resolve sourceName to source and strip sourceName
157
- const normalized = useNormalizedSourceOptions(options)
77
+ // Normalize options: resolve resourceName to resource and strip resourceName
78
+ const normalized = useNormalizedResourceOptions(options)
158
79
 
159
80
  // Use React's useTransition to avoid UI jank when queries change
160
81
  const [isPending, startTransition] = useTransition()
@@ -207,6 +128,6 @@ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
207
128
 
208
129
  // Subscribe to updates and get the current data
209
130
  // useSyncExternalStore ensures the component re-renders when the data changes
210
- const data = useSyncExternalStore(subscribe, getCurrent) as SanityQueryResult
131
+ const data = useSyncExternalStore(subscribe, getCurrent) as unknown
211
132
  return useMemo(() => ({data, isPending}), [data, isPending])
212
133
  }
@@ -43,7 +43,7 @@ describe('useActiveReleases', () => {
43
43
  },
44
44
  {
45
45
  wrapper: ({children}) => (
46
- <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
46
+ <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
47
47
  {children}
48
48
  </ResourceProvider>
49
49
  ),
@@ -75,7 +75,7 @@ describe('useActiveReleases', () => {
75
75
 
76
76
  const {result} = renderHook(() => useActiveReleases(), {
77
77
  wrapper: ({children}) => (
78
- <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
78
+ <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
79
79
  {children}
80
80
  </ResourceProvider>
81
81
  ),
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type DocumentResource,
2
3
  getActiveReleasesState,
3
4
  type ReleaseDocument,
4
5
  type SanityInstance,
@@ -6,14 +7,9 @@ import {
6
7
  } from '@sanity/sdk'
7
8
  import {filter, firstValueFrom} from 'rxjs'
8
9
 
10
+ import {type ResourceHandle} from '../../config/handles'
9
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
10
-
11
- /**
12
- * @public
13
- */
14
- type UseActiveReleases = {
15
- (): ReleaseDocument[]
16
- }
12
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
17
13
 
18
14
  /**
19
15
  * @public
@@ -30,10 +26,26 @@ type UseActiveReleases = {
30
26
  * const activeReleases = useActiveReleases()
31
27
  * ```
32
28
  */
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))),
29
+ type UseActiveReleases = {
30
+ (options?: ResourceHandle | undefined): ReleaseDocument[]
31
+ }
32
+
33
+ const useActiveReleasesValue = createStateSourceHook({
34
+ getState: getActiveReleasesState as (
35
+ instance: SanityInstance,
36
+ options: {resource: DocumentResource},
37
+ ) => StateSource<ReleaseDocument[]>,
38
+ shouldSuspend: (instance: SanityInstance, options: {resource: DocumentResource}) =>
39
+ getActiveReleasesState(instance, options).getCurrent() === undefined,
40
+ suspender: (instance: SanityInstance, options: {resource: DocumentResource}) =>
41
+ firstValueFrom(getActiveReleasesState(instance, options).observable.pipe(filter(Boolean))),
39
42
  })
43
+
44
+ /**
45
+ * @public
46
+ * @function
47
+ */
48
+ export const useActiveReleases: UseActiveReleases = (options: ResourceHandle | undefined) => {
49
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
50
+ return useActiveReleasesValue(normalizedOptions)
51
+ }
@@ -5,11 +5,10 @@ import {
5
5
  type PerspectiveHandle,
6
6
  type ReleaseDocument,
7
7
  } from '@sanity/sdk'
8
- import {renderHook} from '@testing-library/react'
9
8
  import {BehaviorSubject} from 'rxjs'
10
9
  import {describe, expect, it, vi} from 'vitest'
11
10
 
12
- import {ResourceProvider} from '../../context/ResourceProvider'
11
+ import {renderHook} from '../../../test/test-utils'
13
12
  import {usePerspective} from './usePerspective'
14
13
 
15
14
  // Mock the SDK functions
@@ -68,18 +67,13 @@ describe('usePerspective', () => {
68
67
  vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
69
68
  vi.mocked(getActiveReleasesState).mockReturnValue(mockReleasesStateSource)
70
69
 
71
- const {result} = renderHook(
72
- () => {
73
- try {
74
- return usePerspective(perspectiveHandle)
75
- } catch (e) {
76
- return e
77
- }
78
- },
79
- {
80
- wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
81
- },
82
- )
70
+ const {result} = renderHook(() => {
71
+ try {
72
+ return usePerspective(perspectiveHandle)
73
+ } catch (e) {
74
+ return e
75
+ }
76
+ })
83
77
 
84
78
  // Verify that the hook threw a promise (suspended)
85
79
  expect(result.current).toBeInstanceOf(Promise)
@@ -104,9 +98,7 @@ describe('usePerspective', () => {
104
98
 
105
99
  vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
106
100
 
107
- const {result} = renderHook(() => usePerspective(perspectiveHandle), {
108
- wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
109
- })
101
+ const {result} = renderHook(() => usePerspective(perspectiveHandle))
110
102
 
111
103
  // Verify that the hook returned the perspective without suspending
112
104
  expect(result.current).toEqual(mockPerspective)
@@ -1,20 +1,14 @@
1
1
  import {
2
- getActiveReleasesState,
2
+ type DocumentResource,
3
3
  getPerspectiveState,
4
- type PerspectiveHandle,
5
4
  type SanityInstance,
6
5
  type StateSource,
7
6
  } from '@sanity/sdk'
8
7
  import {filter, firstValueFrom} from 'rxjs'
9
8
 
9
+ import {type ResourceHandle} from '../../config/handles'
10
10
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
11
-
12
- /**
13
- * @public
14
- */
15
- type UsePerspective = {
16
- (perspectiveHandle: PerspectiveHandle): string | string[]
17
- }
11
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
18
12
 
19
13
  /**
20
14
  * @public
@@ -29,22 +23,39 @@ type UsePerspective = {
29
23
  * @example
30
24
  * ```tsx
31
25
  * import {usePerspective, useQuery} from '@sanity/sdk-react'
32
-
33
- * const perspective = usePerspective({perspective: 'rxg1346', projectId: 'abc123', dataset: 'production'})
34
- * const {data} = useQuery<Movie[]>('*[_type == "movie"]', {
35
- * perspective: perspective,
26
+ *
27
+ * const perspective = usePerspective({
28
+ * perspective: 'rxg1346',
29
+ * resource: {projectId: 'abc123', dataset: 'production'},
30
+ * })
31
+ * const {data} = useQuery<Movie[]>({
32
+ * query: '*[_type == "movie"]',
33
+ * perspective,
36
34
  * })
37
35
  * ```
38
36
  *
39
37
  * @returns The perspective for the given perspective handle.
40
38
  */
41
- export const usePerspective: UsePerspective = createStateSourceHook({
39
+ type UsePerspective = {
40
+ (perspectiveHandle?: ResourceHandle): string | string[]
41
+ }
42
+
43
+ const usePerspectiveValue = createStateSourceHook({
42
44
  getState: getPerspectiveState as (
43
45
  instance: SanityInstance,
44
- perspectiveHandle?: PerspectiveHandle,
46
+ perspectiveHandle: {resource: DocumentResource; perspective?: unknown},
45
47
  ) => StateSource<string | string[]>,
46
- shouldSuspend: (instance: SanityInstance, options: PerspectiveHandle): boolean =>
48
+ shouldSuspend: (instance: SanityInstance, options: {resource: DocumentResource}): boolean =>
47
49
  getPerspectiveState(instance, options).getCurrent() === undefined,
48
- suspender: (instance: SanityInstance, _options?: PerspectiveHandle) =>
49
- firstValueFrom(getActiveReleasesState(instance).observable.pipe(filter(Boolean))),
50
+ suspender: (instance: SanityInstance, _options?: {resource: DocumentResource}) =>
51
+ firstValueFrom(getPerspectiveState(instance, _options).observable.pipe(filter(Boolean))),
50
52
  })
53
+
54
+ /**
55
+ * @public
56
+ * @function
57
+ */
58
+ export const usePerspective: UsePerspective = (options: ResourceHandle | undefined) => {
59
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
60
+ return usePerspectiveValue(normalizedOptions)
61
+ }
@@ -88,7 +88,7 @@ describe('useUser', () => {
88
88
  }
89
89
 
90
90
  render(
91
- <ResourceProvider projectId="p" fallback={null}>
91
+ <ResourceProvider resource={{projectId: 'p', dataset: 'production'}} fallback={null}>
92
92
  <TestComponent />
93
93
  </ResourceProvider>,
94
94
  )
@@ -340,7 +340,10 @@ describe('useUser', () => {
340
340
  }
341
341
 
342
342
  render(
343
- <ResourceProvider projectId="test-project" fallback={null}>
343
+ <ResourceProvider
344
+ resource={{projectId: 'test-project', dataset: 'production'}}
345
+ fallback={null}
346
+ >
344
347
  <TestComponent />
345
348
  </ResourceProvider>,
346
349
  )
@@ -387,7 +390,10 @@ describe('useUser', () => {
387
390
  }
388
391
 
389
392
  render(
390
- <ResourceProvider projectId="test-project" fallback={null}>
393
+ <ResourceProvider
394
+ resource={{projectId: 'test-project', dataset: 'production'}}
395
+ fallback={null}
396
+ >
391
397
  <WrapperComponent />
392
398
  </ResourceProvider>,
393
399
  )
@@ -58,7 +58,7 @@ export interface UserResult {
58
58
  * ```
59
59
  */
60
60
  export function useUser(options: GetUserOptions): UserResult {
61
- const instance = useSanityInstance(options)
61
+ const instance = useSanityInstance()
62
62
  // Use React's useTransition to avoid UI jank when user options change
63
63
  const [isPending, startTransition] = useTransition()
64
64
 
@@ -313,7 +313,10 @@ describe('useUsers', () => {
313
313
  }
314
314
 
315
315
  render(
316
- <ResourceProvider projectId="p" fallback={<div data-testid="fallback">Loading...</div>}>
316
+ <ResourceProvider
317
+ resource={{projectId: 'p', dataset: 'production'}}
318
+ fallback={<div data-testid="fallback">Loading...</div>}
319
+ >
317
320
  <TestComponent />
318
321
  </ResourceProvider>,
319
322
  )
@@ -327,7 +330,7 @@ describe('useUsers', () => {
327
330
 
328
331
  // Verify that loadMoreUsers was called with the correct arguments
329
332
  expect(loadMoreUsers).toHaveBeenCalledWith(
330
- expect.objectContaining({config: {projectId: 'p'}}),
333
+ expect.objectContaining({instanceId: expect.any(String)}),
331
334
  {
332
335
  resourceType: 'organization',
333
336
  organizationId: 'test-org',
@@ -68,7 +68,7 @@ export interface UsersResult {
68
68
  * ```
69
69
  */
70
70
  export function useUsers(options?: GetUsersOptions): UsersResult {
71
- const instance = useSanityInstance(options)
71
+ const instance = useSanityInstance()
72
72
  // Use React's useTransition to avoid UI jank when user options change
73
73
  const [isPending, startTransition] = useTransition()
74
74
 
@@ -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>>({})