@sanity/sdk-react 0.0.0-rc.5 → 0.0.0-rc.7

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 (44) hide show
  1. package/README.md +5 -57
  2. package/dist/index.d.ts +998 -437
  3. package/dist/index.js +325 -259
  4. package/dist/index.js.map +1 -1
  5. package/package.json +13 -12
  6. package/src/_exports/sdk-react.ts +4 -1
  7. package/src/components/SDKProvider.tsx +6 -1
  8. package/src/components/SanityApp.test.tsx +29 -47
  9. package/src/components/SanityApp.tsx +12 -11
  10. package/src/components/auth/AuthBoundary.test.tsx +177 -7
  11. package/src/components/auth/AuthBoundary.tsx +31 -1
  12. package/src/components/auth/ConfigurationError.ts +22 -0
  13. package/src/components/auth/LoginError.tsx +9 -3
  14. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  15. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  16. package/src/hooks/client/useClient.ts +3 -3
  17. package/src/hooks/comlink/useManageFavorite.test.ts +276 -27
  18. package/src/hooks/comlink/useManageFavorite.ts +102 -51
  19. package/src/hooks/comlink/useWindowConnection.ts +3 -2
  20. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  21. package/src/hooks/datasets/useDatasets.ts +2 -1
  22. package/src/hooks/document/useApplyDocumentActions.ts +105 -31
  23. package/src/hooks/document/useDocument.test.ts +41 -4
  24. package/src/hooks/document/useDocument.ts +198 -114
  25. package/src/hooks/document/useDocumentEvent.test.ts +5 -5
  26. package/src/hooks/document/useDocumentEvent.ts +67 -23
  27. package/src/hooks/document/useDocumentPermissions.ts +47 -8
  28. package/src/hooks/document/useDocumentSyncStatus.test.ts +12 -5
  29. package/src/hooks/document/useDocumentSyncStatus.ts +41 -14
  30. package/src/hooks/document/useEditDocument.test.ts +24 -6
  31. package/src/hooks/document/useEditDocument.ts +238 -133
  32. package/src/hooks/documents/useDocuments.test.tsx +1 -1
  33. package/src/hooks/documents/useDocuments.ts +153 -44
  34. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +1 -1
  35. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +120 -47
  36. package/src/hooks/projection/useProjection.ts +134 -46
  37. package/src/hooks/projects/useProject.test.ts +80 -0
  38. package/src/hooks/projects/useProjects.test.ts +77 -0
  39. package/src/hooks/query/useQuery.test.tsx +4 -4
  40. package/src/hooks/query/useQuery.ts +115 -43
  41. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  42. package/src/hooks/releases/useActiveReleases.ts +39 -0
  43. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  44. package/src/hooks/releases/usePerspective.ts +49 -0
@@ -4,6 +4,7 @@ import {
4
4
  resolveProjection,
5
5
  type ValidProjection,
6
6
  } from '@sanity/sdk'
7
+ import {type SanityProjectionResult} from 'groq'
7
8
  import {useCallback, useSyncExternalStore} from 'react'
8
9
  import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
9
10
 
@@ -13,79 +14,166 @@ import {useSanityInstance} from '../context/useSanityInstance'
13
14
  * @public
14
15
  * @category Types
15
16
  */
16
- export interface UseProjectionOptions extends DocumentHandle {
17
+ export interface UseProjectionOptions<
18
+ TProjection extends ValidProjection = ValidProjection,
19
+ TDocumentType extends string = string,
20
+ TDataset extends string = string,
21
+ TProjectId extends string = string,
22
+ > extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
23
+ /** The GROQ projection string */
24
+ projection: TProjection
25
+ /** Optional parameters for the projection query */
26
+ params?: Record<string, unknown>
27
+ /** Optional ref to track viewport intersection for lazy loading */
17
28
  ref?: React.RefObject<unknown>
18
- projection: ValidProjection
19
29
  }
20
30
 
21
31
  /**
22
32
  * @public
23
33
  * @category Types
24
34
  */
25
- export interface UseProjectionResults<TData extends object> {
35
+ export interface UseProjectionResults<TData> {
36
+ /** The projected data */
26
37
  data: TData
38
+ /** True if the projection is currently being resolved */
27
39
  isPending: boolean
28
40
  }
29
41
 
30
42
  /**
31
43
  * @public
32
44
  *
33
- * Returns the projection values of a document (specified via a `DocumentHandle`),
34
- * based on the provided projection string. These values are live and will update in realtime.
35
- * To reduce unnecessary network requests for resolving the projection values, an optional `ref` can be passed to the hook so that projection
36
- * resolution will only occur if the `ref` is intersecting the current viewport.
45
+ * Returns the projected values of a document based on the provided projection string.
46
+ * These values are live and will update in realtime.
47
+ * To optimize network requests, an optional `ref` can be passed to only resolve the projection
48
+ * when the referenced element is intersecting the viewport.
37
49
  *
38
50
  * @category Documents
39
- * @param options - The document handle for the document you want to project values from, the projection string, and an optional ref
40
- * @returns The projection values for the given document and a boolean to indicate whether the resolution is pending
51
+ * @remarks
52
+ * This hook has multiple signatures allowing for fine-grained control over type inference:
53
+ * - Using Typegen: Infers the return type based on the `documentType`, `dataset`, `projectId`, and `projection`.
54
+ * - Using explicit type parameter: Allows specifying a custom return type `TData`.
41
55
  *
42
- * @example Using a projection to render a preview of document
43
- * ```
44
- * // ProjectionComponent.jsx
45
- * export default function ProjectionComponent({ document }) {
46
- * const ref = useRef(null)
47
- * const { results: { title, coverImage, authors }, isPending } = useProjection({
48
- * document,
56
+ * @param options - An object containing the `DocumentHandle` properties (`documentId`, `documentType`, etc.), the `projection` string, optional `params`, and an optional `ref`.
57
+ * @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.
58
+ */
59
+
60
+ // Overload 1: Relies on Typegen
61
+ /**
62
+ * @beta
63
+ * Fetch a projection, relying on Typegen for the return type based on the handle and projection.
64
+ *
65
+ * @category Documents
66
+ * @param options - Options including the document handle properties (`documentId`, `documentType`, etc.) and the `projection`.
67
+ * @returns The projected data, typed based on Typegen.
68
+ *
69
+ * @example Using Typegen for a book preview
70
+ * ```tsx
71
+ * // ProjectionComponent.tsx
72
+ * import {useProjection, type DocumentHandle} from '@sanity/sdk-react'
73
+ * import {useRef} from 'react'
74
+ * import {defineProjection} from 'groq'
75
+ *
76
+ * // Define props using DocumentHandle with the specific document type
77
+ * type ProjectionComponentProps = {
78
+ * doc: DocumentHandle<'book'> // Typegen knows 'book'
79
+ * }
80
+ *
81
+ * // This is required for typegen to generate the correct return type
82
+ * const myProjection = defineProjection(`{
83
+ * title,
84
+ * 'coverImage': cover.asset->url,
85
+ * 'authors': array::join(authors[]->{'name': firstName + ' ' + lastName}.name, ', ')
86
+ * }`)
87
+ *
88
+ * export default function ProjectionComponent({ doc }: ProjectionComponentProps) {
89
+ * const ref = useRef(null) // Optional ref to track viewport intersection for lazy loading
90
+ *
91
+ * // Spread the doc handle into the options
92
+ * // Typegen infers the return type based on 'book' and the projection
93
+ * const { data } = useProjection({
94
+ * ...doc, // Pass the handle properties
49
95
  * ref,
50
- * projection: `{
51
- * title,
52
- * 'coverImage': cover.asset->url,
53
- * 'authors': array::join(authors[]->{'name': firstName + ' ' + lastName + ' '}.name, ', ')
54
- * }`,
96
+ * projection: myProjection,
55
97
  * })
56
98
  *
99
+ * // Suspense handles initial load, check for data existence after
57
100
  * return (
58
- * <article ref={ref} style={{ opacity: isPending ? 0.5 : 1}}>
59
- * <h2>{title}</h2>
60
- * <img src={coverImage} alt={title} />
61
- * <p>{authors}</p>
101
+ * <article ref={ref}>
102
+ * <h2>{data.title ?? 'Untitled'}</h2>
103
+ * {data.coverImage && <img src={data.coverImage} alt={data.title} />}
104
+ * <p>{data.authors ?? 'Unknown authors'}</p>
62
105
  * </article>
63
106
  * )
64
107
  * }
65
- * ```
66
108
  *
67
- * @example Combining with useDocuments to render a collection with specific fields
109
+ * // Usage:
110
+ * // import {createDocumentHandle} from '@sanity/sdk-react'
111
+ * // const myDocHandle = createDocumentHandle({ documentId: 'book123', documentType: 'book' })
112
+ * // <Suspense fallback='Loading preview...'>
113
+ * // <ProjectionComponent doc={myDocHandle} />
114
+ * // </Suspense>
68
115
  * ```
69
- * // DocumentList.jsx
70
- * const { data } = useDocuments({ filter: '_type == "article"' })
71
- * return (
72
- * <div>
73
- * <h1>Books</h1>
74
- * <ul>
75
- * {data.map(book => (
76
- * <li key={book._id}>
77
- * <Suspense fallback='Loading…'>
78
- * <ProjectionComponent
79
- * document={book}
80
- * />
81
- * </Suspense>
82
- * </li>
83
- * ))}
84
- * </ul>
85
- * </div>
86
- * )
116
+ */
117
+ export function useProjection<
118
+ TProjection extends ValidProjection = ValidProjection,
119
+ TDocumentType extends string = string,
120
+ TDataset extends string = string,
121
+ TProjectId extends string = string,
122
+ >(
123
+ options: UseProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
124
+ ): UseProjectionResults<SanityProjectionResult<TProjection, TDocumentType, TDataset, TProjectId>>
125
+
126
+ // Overload 2: Explicit type provided
127
+ /**
128
+ * @beta
129
+ * Fetch a projection with an explicitly defined return type `TData`.
130
+ *
131
+ * @param options - Options including the document handle properties (`documentId`, etc.) and the `projection`.
132
+ * @returns The projected data, cast to the explicit type `TData`.
133
+ *
134
+ * @example Explicitly typing the projection result
135
+ * ```tsx
136
+ * import {useProjection, type DocumentHandle} from '@sanity/sdk-react'
137
+ * import {useRef} from 'react'
138
+ *
139
+ * interface SimpleBookPreview {
140
+ * title?: string;
141
+ * authorName?: string;
142
+ * }
143
+ *
144
+ * type BookPreviewProps = {
145
+ * doc: DocumentHandle
146
+ * }
147
+ *
148
+ * function BookPreview({ doc }: BookPreviewProps) {
149
+ * const ref = useRef(null)
150
+ * const { data } = useProjection<SimpleBookPreview>({
151
+ * ...doc,
152
+ * ref,
153
+ * projection: `{ title, 'authorName': author->name }`
154
+ * })
155
+ *
156
+ * return (
157
+ * <div ref={ref}>
158
+ * <h3>{data.title ?? 'No Title'}</h3>
159
+ * <p>By: {data.authorName ?? 'Unknown'}</p>
160
+ * </div>
161
+ * )
162
+ * }
163
+ *
164
+ * // Usage:
165
+ * // import {createDocumentHandle} from '@sanity/sdk-react'
166
+ * // const doc = createDocumentHandle({ documentId: 'abc', documentType: 'book' })
167
+ * // <Suspense fallback='Loading...'>
168
+ * // <BookPreview doc={doc} />
169
+ * // </Suspense>
87
170
  * ```
88
171
  */
172
+ export function useProjection<TData extends object>(
173
+ options: UseProjectionOptions, // Uses base options type
174
+ ): UseProjectionResults<TData>
175
+
176
+ // Implementation (no JSDoc needed here as it's covered by overloads)
89
177
  export function useProjection<TData extends object>({
90
178
  ref,
91
179
  projection,
@@ -94,7 +182,7 @@ export function useProjection<TData extends object>({
94
182
  const instance = useSanityInstance()
95
183
  const stateSource = getProjectionState<TData>(instance, {...docHandle, projection})
96
184
 
97
- if (stateSource.getCurrent().data === null) {
185
+ if (stateSource.getCurrent()?.data === null) {
98
186
  throw resolveProjection(instance, {...docHandle, projection})
99
187
  }
100
188
 
@@ -0,0 +1,80 @@
1
+ import {getProjectState, type ProjectHandle, type SanityInstance} from '@sanity/sdk'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
+
6
+ // Mock dependencies
7
+ vi.mock('@sanity/sdk', () => ({
8
+ getProjectState: vi.fn(() => ({
9
+ getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
10
+ })),
11
+ resolveProject: vi.fn(),
12
+ }))
13
+ vi.mock('../helpers/createStateSourceHook', () => ({
14
+ createStateSourceHook: vi.fn(),
15
+ }))
16
+
17
+ describe('useProject', () => {
18
+ // Use beforeEach to reset modules and ensure mocks are fresh for each test
19
+ beforeEach(() => {
20
+ vi.resetModules()
21
+ // Re-mock dependencies for each test after resetModules
22
+ vi.mock('@sanity/sdk', () => ({
23
+ getProjectState: vi.fn(() => ({
24
+ getCurrent: vi.fn(() => undefined),
25
+ })),
26
+ resolveProject: vi.fn(),
27
+ }))
28
+ vi.mock('../helpers/createStateSourceHook', () => ({
29
+ createStateSourceHook: vi.fn(),
30
+ }))
31
+ })
32
+
33
+ it('should call createStateSourceHook with correct arguments on import', async () => {
34
+ // Dynamically import the hook *after* mocks are set up and modules reset
35
+ await import('./useProject')
36
+
37
+ // Check if createStateSourceHook was called during the module evaluation (import)
38
+ expect(createStateSourceHook).toHaveBeenCalled()
39
+ expect(createStateSourceHook).toHaveBeenCalledWith(
40
+ expect.objectContaining({
41
+ getState: expect.any(Function),
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
45
+ }),
46
+ )
47
+ })
48
+
49
+ it('shouldSuspend should call getProjectState and getCurrent', async () => {
50
+ // Dynamically import the hook *after* mocks are set up and modules reset
51
+ await import('./useProject')
52
+
53
+ // Get the arguments passed to createStateSourceHook
54
+ // Need to ensure createStateSourceHook mock is correctly typed for access
55
+ const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
56
+ expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
57
+ const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
58
+ const shouldSuspend = createStateSourceHookArgs.shouldSuspend
59
+
60
+ // Mock instance and projectHandle for the test call
61
+ const mockInstance = {} as SanityInstance // Use specific type
62
+ const mockProjectHandle = {} as ProjectHandle // Use specific type
63
+
64
+ // Call the shouldSuspend function
65
+ const result = shouldSuspend(mockInstance, mockProjectHandle)
66
+
67
+ // Assert that getProjectState was called with the correct arguments
68
+ // Need to ensure getProjectState mock is correctly typed for access
69
+ const mockGetProjectState = getProjectState as ReturnType<typeof vi.fn>
70
+ expect(mockGetProjectState).toHaveBeenCalledWith(mockInstance, mockProjectHandle)
71
+
72
+ // Assert that getCurrent was called on the result of getProjectState
73
+ expect(mockGetProjectState.mock.results.length).toBeGreaterThan(0)
74
+ const getProjectStateMockResult = mockGetProjectState.mock.results[0].value
75
+ expect(getProjectStateMockResult.getCurrent).toHaveBeenCalled()
76
+
77
+ // Assert the result of shouldSuspend based on the mocked getCurrent value
78
+ expect(result).toBe(true) // Since getCurrent is mocked to return undefined
79
+ })
80
+ })
@@ -0,0 +1,77 @@
1
+ import {getProjectsState, type SanityInstance} from '@sanity/sdk'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
+
6
+ // Mock dependencies
7
+ vi.mock('@sanity/sdk', () => ({
8
+ getProjectsState: vi.fn(() => ({
9
+ getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
10
+ })),
11
+ resolveProjects: vi.fn(),
12
+ }))
13
+ vi.mock('../helpers/createStateSourceHook', () => ({
14
+ createStateSourceHook: vi.fn(),
15
+ }))
16
+
17
+ describe('useProjects', () => {
18
+ // Use beforeEach to reset modules and ensure mocks are fresh for each test
19
+ beforeEach(() => {
20
+ vi.resetModules()
21
+ // Re-mock dependencies for each test after resetModules
22
+ vi.mock('@sanity/sdk', () => ({
23
+ getProjectsState: vi.fn(() => ({
24
+ getCurrent: vi.fn(() => undefined),
25
+ })),
26
+ resolveProjects: vi.fn(),
27
+ }))
28
+ vi.mock('../helpers/createStateSourceHook', () => ({
29
+ createStateSourceHook: vi.fn(),
30
+ }))
31
+ })
32
+
33
+ it('should call createStateSourceHook with correct arguments on import', async () => {
34
+ // Dynamically import the hook *after* mocks are set up and modules reset
35
+ await import('./useProjects')
36
+
37
+ // Check if createStateSourceHook was called during the module evaluation (import)
38
+ expect(createStateSourceHook).toHaveBeenCalled()
39
+ expect(createStateSourceHook).toHaveBeenCalledWith(
40
+ expect.objectContaining({
41
+ getState: expect.any(Function),
42
+ shouldSuspend: expect.any(Function),
43
+ suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
44
+ // Note: getConfig is not used in useProjects
45
+ }),
46
+ )
47
+ })
48
+
49
+ it('shouldSuspend should call getProjectsState and getCurrent', async () => {
50
+ // Dynamically import the hook *after* mocks are set up and modules reset
51
+ await import('./useProjects')
52
+
53
+ // Get the arguments passed to createStateSourceHook
54
+ const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
55
+ expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
56
+ const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
57
+ const shouldSuspend = createStateSourceHookArgs.shouldSuspend
58
+
59
+ // Mock instance for the test call
60
+ const mockInstance = {} as SanityInstance // Use specific type
61
+
62
+ // Call the shouldSuspend function
63
+ const result = shouldSuspend(mockInstance)
64
+
65
+ // Assert that getProjectsState was called with the correct arguments
66
+ const mockGetProjectsState = getProjectsState as ReturnType<typeof vi.fn>
67
+ expect(mockGetProjectsState).toHaveBeenCalledWith(mockInstance)
68
+
69
+ // Assert that getCurrent was called on the result of getProjectsState
70
+ expect(mockGetProjectsState.mock.results.length).toBeGreaterThan(0)
71
+ const getProjectsStateMockResult = mockGetProjectsState.mock.results[0].value
72
+ expect(getProjectsStateMockResult.getCurrent).toHaveBeenCalled()
73
+
74
+ // Assert the result of shouldSuspend based on the mocked getCurrent value
75
+ expect(result).toBe(true) // Since getCurrent is mocked to return undefined
76
+ })
77
+ })
@@ -37,7 +37,7 @@ describe('useQuery', () => {
37
37
  } as StateSource<unknown>)
38
38
 
39
39
  function TestComponent() {
40
- const {data, isPending} = useQuery<string>('test query')
40
+ const {data, isPending} = useQuery({query: 'test query'})
41
41
  return (
42
42
  <div data-testid="output">
43
43
  {data} - {isPending ? 'pending' : 'not pending'}
@@ -82,7 +82,7 @@ describe('useQuery', () => {
82
82
  )
83
83
 
84
84
  function TestComponent() {
85
- const {data} = useQuery<string>('test query')
85
+ const {data} = useQuery({query: 'test query'})
86
86
  return <div data-testid="output">{data}</div>
87
87
  }
88
88
 
@@ -108,7 +108,7 @@ describe('useQuery', () => {
108
108
  const getCurrent = vi.fn(() => ref.current)
109
109
  const storeChanged$ = new Subject<void>()
110
110
 
111
- vi.mocked(getQueryState).mockImplementation((_instance, query) => {
111
+ vi.mocked(getQueryState).mockImplementation((_instance, {query}) => {
112
112
  if (query === 'query1') {
113
113
  return {
114
114
  getCurrent: vi.fn().mockReturnValue('data1'),
@@ -146,7 +146,7 @@ describe('useQuery', () => {
146
146
 
147
147
  function WrapperComponent() {
148
148
  const [query, setQuery] = useState('query1')
149
- const {data, isPending} = useQuery<string>(query)
149
+ const {data, isPending} = useQuery<string>({query})
150
150
  return (
151
151
  <div>
152
152
  <div data-testid="output">
@@ -5,71 +5,143 @@ import {
5
5
  type QueryOptions,
6
6
  resolveQuery,
7
7
  } from '@sanity/sdk'
8
+ import {type SanityQueryResult} from 'groq'
8
9
  import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
9
10
 
10
11
  import {useSanityInstance} from '../context/useSanityInstance'
11
12
 
13
+ // Overload 1: Inferred Type (using Typegen)
12
14
  /**
13
- * Executes GROQ queries against a Sanity dataset.
15
+ * @beta
16
+ * Executes a GROQ query, inferring the result type from the query string and options.
17
+ * Leverages Sanity Typegen if configured for enhanced type safety.
14
18
  *
15
- * This hook provides a convenient way to fetch and subscribe to real-time updates
16
- * for your Sanity content. Changes made to the dataset's content will trigger
17
- * automatic updates.
19
+ * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
20
+ * @returns An object containing `data` (typed based on the query) and `isPending` (for transitions).
18
21
  *
19
- * @remarks
20
- * The returned `isPending` flag indicates when a React transition is in progress,
21
- * which can be used to show loading states for query changes.
22
+ * @example Basic usage (Inferred Type)
23
+ * ```tsx
24
+ * import {useQuery} from '@sanity/sdk-react'
25
+ * import {defineQuery} from 'groq'
22
26
  *
23
- * @beta
24
- * @category GROQ
25
- * @param query - GROQ query string to execute
26
- * @param options - Optional configuration for the query, including projectId and dataset
27
- * @returns Object containing the query result and a pending state flag
27
+ * const myQuery = defineQuery(`*[_type == "movie"]{_id, title}`)
28
28
  *
29
- * @example Basic usage
30
- * ```tsx
31
- * const {data, isPending} = useQuery<Movie[]>('*[_type == "movie"]')
32
- * ```
29
+ * function MovieList() {
30
+ * // Typegen infers the return type for data
31
+ * const {data} = useQuery({ query: myQuery })
33
32
  *
34
- * @example Using parameters
35
- * ```tsx
36
- * // With parameters
37
- * const {data} = useQuery<Movie>('*[_type == "movie" && _id == $id][0]', {
38
- * params: { id: 'movie-123' }
39
- * })
33
+ * return (
34
+ * <div>
35
+ * <h2>Movies</h2>
36
+ * <ul>
37
+ * {data.map(movie => <li key={movie._id}>{movie.title}</li>)}
38
+ * </ul>
39
+ * </div>
40
+ * )
41
+ * }
42
+ * // Suspense boundary should wrap <MovieList /> for initial load
40
43
  * ```
41
44
  *
42
- * @example Query from a specific project/dataset
45
+ * @example Using parameters (Inferred Type)
43
46
  * ```tsx
44
- * // Specify which project and dataset to query
45
- * const {data} = useQuery<Movie[]>('*[_type == "movie"]', {
46
- * projectId: 'abc123',
47
- * dataset: 'production'
48
- * })
47
+ * import {useQuery} from '@sanity/sdk-react'
48
+ * import {defineQuery} from 'groq'
49
+ *
50
+ * const myQuery = defineQuery(`*[_type == "movie" && _id == $id][0]`)
51
+ *
52
+ * function MovieDetails({movieId}: {movieId: string}) {
53
+ * // Typegen infers the return type based on query and params
54
+ * const {data, isPending} = useQuery({
55
+ * query: myQuery,
56
+ * params: { id: movieId }
57
+ * })
58
+ *
59
+ * return (
60
+ * // utilize `isPending` to signal to users that new data is coming in
61
+ * // (e.g. the `movieId` changed and we're loading in the new one)
62
+ * <div style={{ opacity: isPending ? 0.5 : 1 }}>
63
+ * {data ? <h1>{data.title}</h1> : <p>Movie not found</p>}
64
+ * </div>
65
+ * )
66
+ * }
49
67
  * ```
68
+ */
69
+ export function useQuery<
70
+ TQuery extends string = string,
71
+ TDataset extends string = string,
72
+ TProjectId extends string = string,
73
+ >(
74
+ options: QueryOptions<TQuery, TDataset, TProjectId>,
75
+ ): {
76
+ /** The query result, typed based on the GROQ query string */
77
+ data: SanityQueryResult<TQuery, TDataset, TProjectId>
78
+ /** True if a query transition is in progress */
79
+ isPending: boolean
80
+ }
81
+
82
+ // Overload 2: Explicit Type Provided
83
+ /**
84
+ * @beta
85
+ * Executes a GROQ query with an explicitly provided result type `TData`.
50
86
  *
51
- * @example With a loading state for transitions
87
+ * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
88
+ * @returns An object containing `data` (cast to `TData`) and `isPending` (indicates whether a query resolution is pending; note that Suspense handles initial loading states). *
89
+ * @example Manually typed query result
52
90
  * ```tsx
53
- * const {data, isPending} = useQuery<Movie[]>('*[_type == "movie"]')
54
- * return (
55
- * <div>
56
- * {isPending && <div>Updating...</div>}
57
- * <ul>
58
- * {data.map(movie => <li key={movie._id}>{movie.title}</li>)}
59
- * </ul>
60
- * </div>
61
- * )
91
+ * import {useQuery} from '@sanity/sdk-react'
92
+ *
93
+ * interface CustomMovieTitle {
94
+ * movieTitle?: string
95
+ * }
96
+ *
97
+ * function FirstMovieTitle() {
98
+ * // Provide the explicit type TData
99
+ * const {data, isPending} = useQuery<CustomMovieTitle>({
100
+ * query: '*[_type == "movie"][0]{ "movieTitle": title }'
101
+ * })
102
+ *
103
+ * return (
104
+ * <h1 style={{ opacity: isPending ? 0.5 : 1 }}>
105
+ * {data?.movieTitle ?? 'No title found'}
106
+ * </h1>
107
+ * )
108
+ * }
62
109
  * ```
110
+ */
111
+ export function useQuery<TData>(options: QueryOptions): {
112
+ /** The query result, cast to the provided type TData */
113
+ data: TData
114
+ /** True if another query is resolving in the background (suspense handles the initial loading state) */
115
+ isPending: boolean
116
+ }
117
+
118
+ /**
119
+ * @beta
120
+ * Fetches data and subscribes to real-time updates using a GROQ query.
63
121
  *
122
+ * @remarks
123
+ * This hook provides a convenient way to fetch data from your Sanity dataset and
124
+ * automatically receive updates in real-time when the queried data changes.
125
+ *
126
+ * Features:
127
+ * - Executes any valid GROQ query.
128
+ * - Subscribes to changes, providing real-time updates.
129
+ * - Integrates with React Suspense for handling initial loading states.
130
+ * - Uses React Transitions for managing loading states during query/parameter changes (indicated by `isPending`).
131
+ * - Supports type inference based on the GROQ query when using Sanity Typegen.
132
+ * - Allows specifying an explicit return type `TData` for the query result.
133
+ *
134
+ * @category GROQ
64
135
  */
65
- export function useQuery<T>(query: string, options?: QueryOptions): {data: T; isPending: boolean} {
136
+ export function useQuery(options: QueryOptions): {data: unknown; isPending: boolean} {
137
+ // Implementation returns unknown, overloads define specifics
66
138
  const instance = useSanityInstance(options)
67
139
 
68
140
  // Use React's useTransition to avoid UI jank when queries change
69
141
  const [isPending, startTransition] = useTransition()
70
142
 
71
143
  // Get the unique key for this query and its options
72
- const queryKey = getQueryKey(query, options)
144
+ const queryKey = getQueryKey(options)
73
145
  // Use a deferred state to avoid immediate re-renders when the query changes
74
146
  const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
75
147
  // Parse the deferred query key back into a query and options
@@ -95,7 +167,7 @@ export function useQuery<T>(query: string, options?: QueryOptions): {data: T; is
95
167
 
96
168
  // Get the state source for this query from the query store
97
169
  const {getCurrent, subscribe} = useMemo(
98
- () => getQueryState(instance, deferred.query, deferred.options),
170
+ () => getQueryState(instance, deferred),
99
171
  [instance, deferred],
100
172
  )
101
173
 
@@ -111,11 +183,11 @@ export function useQuery<T>(query: string, options?: QueryOptions): {data: T; is
111
183
  // Thus, the promise thrown here uses a stable abort signal, ensuring correct behavior.
112
184
  const currentSignal = ref.current.signal
113
185
  // eslint-disable-next-line react-compiler/react-compiler
114
- throw resolveQuery(instance, deferred.query, {...deferred.options, signal: currentSignal})
186
+ throw resolveQuery(instance, {...deferred, signal: currentSignal})
115
187
  }
116
188
 
117
189
  // Subscribe to updates and get the current data
118
190
  // useSyncExternalStore ensures the component re-renders when the data changes
119
- const data = useSyncExternalStore(subscribe, getCurrent) as T
191
+ const data = useSyncExternalStore(subscribe, getCurrent) as SanityQueryResult
120
192
  return useMemo(() => ({data, isPending}), [data, isPending])
121
193
  }