@sanity/sdk-react 0.0.0-rc.6 → 0.0.0-rc.8
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.
- package/README.md +5 -57
- package/dist/index.d.ts +998 -437
- package/dist/index.js +324 -258
- package/dist/index.js.map +1 -1
- package/package.json +16 -15
- package/src/_exports/sdk-react.ts +4 -1
- package/src/components/SDKProvider.tsx +6 -1
- package/src/components/SanityApp.test.tsx +29 -47
- package/src/components/SanityApp.tsx +12 -11
- package/src/components/auth/AuthBoundary.test.tsx +177 -7
- package/src/components/auth/AuthBoundary.tsx +31 -1
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginError.tsx +9 -3
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +3 -3
- package/src/hooks/comlink/useManageFavorite.test.ts +276 -27
- package/src/hooks/comlink/useManageFavorite.ts +102 -51
- package/src/hooks/comlink/useWindowConnection.ts +3 -2
- package/src/hooks/document/useApplyDocumentActions.ts +105 -31
- package/src/hooks/document/useDocument.test.ts +41 -4
- package/src/hooks/document/useDocument.ts +198 -114
- package/src/hooks/document/useDocumentEvent.test.ts +5 -5
- package/src/hooks/document/useDocumentEvent.ts +67 -23
- package/src/hooks/document/useDocumentPermissions.ts +47 -8
- package/src/hooks/document/useDocumentSyncStatus.test.ts +12 -5
- package/src/hooks/document/useDocumentSyncStatus.ts +41 -14
- package/src/hooks/document/useEditDocument.test.ts +24 -6
- package/src/hooks/document/useEditDocument.ts +238 -133
- package/src/hooks/documents/useDocuments.test.tsx +1 -1
- package/src/hooks/documents/useDocuments.ts +153 -44
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +1 -1
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +120 -47
- package/src/hooks/projection/useProjection.ts +134 -46
- package/src/hooks/query/useQuery.test.tsx +4 -4
- package/src/hooks/query/useQuery.ts +115 -43
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- 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
|
|
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
|
|
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
|
|
34
|
-
*
|
|
35
|
-
* To
|
|
36
|
-
*
|
|
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
|
-
* @
|
|
40
|
-
*
|
|
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
|
-
* @
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
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}
|
|
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
|
-
*
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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()
|
|
185
|
+
if (stateSource.getCurrent()?.data === null) {
|
|
98
186
|
throw resolveProjection(instance, {...docHandle, projection})
|
|
99
187
|
}
|
|
100
188
|
|
|
@@ -37,7 +37,7 @@ describe('useQuery', () => {
|
|
|
37
37
|
} as StateSource<unknown>)
|
|
38
38
|
|
|
39
39
|
function TestComponent() {
|
|
40
|
-
const {data, isPending} = useQuery
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
* @
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* @example Basic usage (Inferred Type)
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import {useQuery} from '@sanity/sdk-react'
|
|
25
|
+
* import {defineQuery} from 'groq'
|
|
22
26
|
*
|
|
23
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ```
|
|
29
|
+
* function MovieList() {
|
|
30
|
+
* // Typegen infers the return type for data
|
|
31
|
+
* const {data} = useQuery({ query: myQuery })
|
|
33
32
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
45
|
+
* @example Using parameters (Inferred Type)
|
|
43
46
|
* ```tsx
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
191
|
+
const data = useSyncExternalStore(subscribe, getCurrent) as SanityQueryResult
|
|
120
192
|
return useMemo(() => ({data, isPending}), [data, isPending])
|
|
121
193
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {getActiveReleasesState, type ReleaseDocument, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
3
|
+
import {BehaviorSubject} from 'rxjs'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {useActiveReleases} from './useActiveReleases'
|
|
8
|
+
|
|
9
|
+
// Mock the useSanityInstance hook
|
|
10
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
11
|
+
useSanityInstance: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
// Mock the getActiveReleasesState function
|
|
15
|
+
vi.mock('@sanity/sdk', async () => {
|
|
16
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
getActiveReleasesState: vi.fn(),
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('useActiveReleases', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should suspend when initial state is undefined', () => {
|
|
29
|
+
const mockInstance = {} as SanityInstance
|
|
30
|
+
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
31
|
+
|
|
32
|
+
const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
|
|
33
|
+
const mockStateSource = {
|
|
34
|
+
subscribe: vi.fn((callback) => {
|
|
35
|
+
const subscription = mockSubject.subscribe(callback)
|
|
36
|
+
return () => subscription.unsubscribe()
|
|
37
|
+
}),
|
|
38
|
+
getCurrent: vi.fn(() => undefined),
|
|
39
|
+
observable: mockSubject,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
vi.mocked(getActiveReleasesState).mockReturnValue(mockStateSource)
|
|
43
|
+
|
|
44
|
+
const {result} = renderHook(() => {
|
|
45
|
+
try {
|
|
46
|
+
return useActiveReleases()
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return e
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Verify that the hook threw a promise (suspended)
|
|
53
|
+
expect(result.current).toBeInstanceOf(Promise)
|
|
54
|
+
expect(mockStateSource.getCurrent).toHaveBeenCalled()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should resolve with releases when data is available', () => {
|
|
58
|
+
const mockInstance = {} as SanityInstance
|
|
59
|
+
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
60
|
+
|
|
61
|
+
const mockReleases: ReleaseDocument[] = [
|
|
62
|
+
{_id: 'release1', _type: 'release'} as ReleaseDocument,
|
|
63
|
+
{_id: 'release2', _type: 'release'} as ReleaseDocument,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
|
|
67
|
+
const mockStateSource = {
|
|
68
|
+
subscribe: vi.fn((callback) => {
|
|
69
|
+
const subscription = mockSubject.subscribe(callback)
|
|
70
|
+
return () => subscription.unsubscribe()
|
|
71
|
+
}),
|
|
72
|
+
getCurrent: vi.fn(() => mockReleases),
|
|
73
|
+
observable: mockSubject,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
vi.mocked(getActiveReleasesState).mockReturnValue(mockStateSource)
|
|
77
|
+
|
|
78
|
+
const {result} = renderHook(() => useActiveReleases())
|
|
79
|
+
|
|
80
|
+
// Verify that the hook returned the releases without suspending
|
|
81
|
+
expect(result.current).toEqual(mockReleases)
|
|
82
|
+
expect(mockStateSource.getCurrent).toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getActiveReleasesState,
|
|
3
|
+
type ReleaseDocument,
|
|
4
|
+
type SanityInstance,
|
|
5
|
+
type StateSource,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {filter, firstValueFrom} from 'rxjs'
|
|
8
|
+
|
|
9
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
type UseActiveReleases = {
|
|
15
|
+
(): ReleaseDocument[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @public
|
|
20
|
+
|
|
21
|
+
* Returns the active releases for the current project,
|
|
22
|
+
* represented as a list of release documents.
|
|
23
|
+
*
|
|
24
|
+
* @returns The active releases for the current project.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import {useActiveReleases} from '@sanity/sdk-react'
|
|
29
|
+
*
|
|
30
|
+
* const activeReleases = useActiveReleases()
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
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))),
|
|
39
|
+
})
|