@sanity/sdk-react 3.12.0 → 3.13.0-app-functions.20261106191857
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/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -27
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/_exports/sdk-react.ts +6 -0
- package/src/components/SDKProvider.reporting.test.tsx +69 -0
- package/src/components/SDKProvider.tsx +5 -0
- package/src/components/auth/AuthBoundary.reporting.test.tsx +80 -0
- package/src/components/auth/AuthBoundary.tsx +6 -0
- package/src/components/errors/reportBoundaryError.test.ts +38 -0
- package/src/components/errors/reportBoundaryError.ts +33 -0
- package/src/context/AppFunctionsContext.ts +13 -0
- package/src/context/AppFunctionsProvider.tsx +20 -0
- package/src/hooks/client/useClient.ts +27 -13
- package/src/hooks/functions/useFunctions.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0-app-functions.20261106191857",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@module-federation/runtime": "^2.9.
|
|
46
|
+
"@module-federation/runtime": "^2.9.3",
|
|
47
47
|
"@sanity/client": "^8.14.1",
|
|
48
48
|
"@sanity/message-protocol": "^0.24.0",
|
|
49
|
-
"@sanity/sdk": "3.
|
|
49
|
+
"@sanity/sdk": "3.13.0-app-functions.20261106191857",
|
|
50
50
|
"@sanity/types": "^6.21.0",
|
|
51
51
|
"react-compiler-runtime": "^1.0.0",
|
|
52
52
|
"react-error-boundary": "^6.1.6",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"@types/node": "^24.19.2",
|
|
67
67
|
"@types/react": "^19.3.0",
|
|
68
68
|
"@types/react-dom": "^19.3.0",
|
|
69
|
-
"@vitejs/plugin-react": "^6.1
|
|
70
|
-
"@vitest/coverage-v8": "^5.0
|
|
69
|
+
"@vitejs/plugin-react": "^6.2.1",
|
|
70
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
71
71
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
72
|
-
"groq": "^6.
|
|
72
|
+
"groq": "^6.22.0",
|
|
73
73
|
"groq-js": "^2.0.1",
|
|
74
74
|
"jsdom": "^30.1.3",
|
|
75
75
|
"oxfmt": "^0.75.0",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"react-dom": "^19.3.1",
|
|
79
79
|
"rolldown": "^1.2.12",
|
|
80
80
|
"typescript": "^7.0.2",
|
|
81
|
-
"vite": "^8.3.
|
|
82
|
-
"vitest": "^5.0
|
|
81
|
+
"vite": "^8.3.6",
|
|
82
|
+
"vitest": "^5.1.0",
|
|
83
83
|
"xstate": "5.33.3"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
@@ -5,6 +5,11 @@ export {AuthBoundary, type AuthBoundaryProps} from '../components/auth/AuthBound
|
|
|
5
5
|
export {SanityApp, type SanityAppProps} from '../components/SanityApp'
|
|
6
6
|
export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
|
|
7
7
|
export {type DocumentHandle, type DocumentTypeHandle, type ResourceHandle} from '../config/handles'
|
|
8
|
+
export {
|
|
9
|
+
type AppFunctionsConfig,
|
|
10
|
+
AppFunctionsProvider,
|
|
11
|
+
type AppFunctionsProviderProps,
|
|
12
|
+
} from '../context/AppFunctionsProvider'
|
|
8
13
|
export {ComlinkTokenRefreshProvider} from '../context/ComlinkTokenRefresh'
|
|
9
14
|
export {renderSanityApp} from '../context/renderSanityApp'
|
|
10
15
|
export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
|
|
@@ -73,6 +78,7 @@ export {
|
|
|
73
78
|
type DocumentsResponse,
|
|
74
79
|
useDocuments,
|
|
75
80
|
} from '../hooks/documents/useDocuments'
|
|
81
|
+
export {useFunctions} from '../hooks/functions/useFunctions'
|
|
76
82
|
export {type FetcherHookResult} from '../hooks/helpers/createFetcherHook'
|
|
77
83
|
export {type MutationHookResult} from '../hooks/helpers/createMutationHook'
|
|
78
84
|
export {useInstallation} from '../hooks/installations/useInstallation'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {reportError} from '@sanity/sdk/_internal'
|
|
2
|
+
import {render, screen} from '@testing-library/react'
|
|
3
|
+
import {Component, type ReactNode} from 'react'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {SDKProvider} from './SDKProvider'
|
|
7
|
+
|
|
8
|
+
// Renders the real `react-error-boundary`, which skips `onError` when the fallback rethrows.
|
|
9
|
+
vi.mock('@sanity/sdk/_internal', async (importOriginal) => ({
|
|
10
|
+
...(await importOriginal<object>()),
|
|
11
|
+
reportError: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
vi.mock('../context/ResourceProvider', () => ({
|
|
14
|
+
ResourceProvider: ({children}: {children: ReactNode}) => children,
|
|
15
|
+
}))
|
|
16
|
+
vi.mock('./auth/AuthBoundary', () => ({
|
|
17
|
+
AuthBoundary: ({children}: {children: ReactNode}) => children,
|
|
18
|
+
}))
|
|
19
|
+
vi.mock('../context/OrganizationResourcesProvider', () => ({
|
|
20
|
+
OrganizationResourcesProvider: ({children}: {children: ReactNode}) => children,
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
class AppBoundary extends Component<{children: ReactNode}, {caught: boolean}> {
|
|
24
|
+
override state = {caught: false}
|
|
25
|
+
static getDerivedStateFromError() {
|
|
26
|
+
return {caught: true}
|
|
27
|
+
}
|
|
28
|
+
override render() {
|
|
29
|
+
return this.state.caught ? 'app boundary caught it' : this.props.children
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Thrower({error}: {error: Error}): never {
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('SDKProvider error reporting', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.mocked(reportError).mockClear()
|
|
40
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('reports an error it rethrows, and the app boundary still receives it', () => {
|
|
44
|
+
const error = new TypeError('sdk bug')
|
|
45
|
+
render(
|
|
46
|
+
<AppBoundary>
|
|
47
|
+
<SDKProvider config={{projectId: 'p', dataset: 'd'}} fallback={null}>
|
|
48
|
+
<Thrower error={error} />
|
|
49
|
+
</SDKProvider>
|
|
50
|
+
</AppBoundary>,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(screen.getByText('app boundary caught it')).toBeInTheDocument()
|
|
54
|
+
expect(reportError).toHaveBeenCalledWith(error, {errorBoundary: 'SDKProvider'})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not report a chunk load error, which it renders itself', () => {
|
|
58
|
+
const error = Object.assign(new Error('Loading chunk 5 failed.'), {name: 'ChunkLoadError'})
|
|
59
|
+
render(
|
|
60
|
+
<AppBoundary>
|
|
61
|
+
<SDKProvider config={{projectId: 'p', dataset: 'd'}} fallback={null}>
|
|
62
|
+
<Thrower error={error} />
|
|
63
|
+
</SDKProvider>
|
|
64
|
+
</AppBoundary>,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(reportError).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -13,6 +13,9 @@ import {ResourceProvider} from '../context/ResourceProvider'
|
|
|
13
13
|
import {AuthBoundary, type AuthBoundaryProps} from './auth/AuthBoundary'
|
|
14
14
|
import {ChunkLoadError} from './errors/ChunkLoadError'
|
|
15
15
|
import {clearChunkReloadFlag} from './errors/chunkReloadStorage'
|
|
16
|
+
import {reportBoundaryError} from './errors/reportBoundaryError'
|
|
17
|
+
|
|
18
|
+
const reportSDKProviderError = reportBoundaryError('SDKProvider')
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* @internal
|
|
@@ -119,6 +122,8 @@ function ChunkAwareFallback(fallbackProps: FallbackProps): ReactElement {
|
|
|
119
122
|
if (isImportError(fallbackProps.error)) {
|
|
120
123
|
return <ChunkLoadError {...fallbackProps} />
|
|
121
124
|
}
|
|
125
|
+
// Report here, not in `onError`: React skips `onError` on a boundary whose fallback rethrows
|
|
126
|
+
reportSDKProviderError(fallbackProps.error)
|
|
122
127
|
// Re-throw so downstream boundaries (e.g. AuthBoundary) handle other errors.
|
|
123
128
|
throw fallbackProps.error
|
|
124
129
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {reportError} from '@sanity/sdk/_internal'
|
|
3
|
+
import {render, screen} from '@testing-library/react'
|
|
4
|
+
import {Component, type ReactNode} from 'react'
|
|
5
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {AuthBoundary} from './AuthBoundary'
|
|
8
|
+
import {AuthError} from './AuthError'
|
|
9
|
+
|
|
10
|
+
// These tests render the real `react-error-boundary`. `AuthBoundary.test.tsx` replaces it with a
|
|
11
|
+
// mock that always calls `componentDidCatch`, which React does not do for a boundary whose
|
|
12
|
+
// fallback rethrows.
|
|
13
|
+
vi.mock('@sanity/sdk/_internal', async (importOriginal) => ({
|
|
14
|
+
...(await importOriginal<object>()),
|
|
15
|
+
reportError: vi.fn(),
|
|
16
|
+
}))
|
|
17
|
+
vi.mock('../../hooks/auth/useAuthState', () => ({
|
|
18
|
+
useAuthState: vi.fn(() => ({type: 'logged-in', token: 'token', currentUser: null})),
|
|
19
|
+
}))
|
|
20
|
+
const instance = createSanityInstance()
|
|
21
|
+
vi.mock('../../hooks/context/useSanityInstance', () => ({useSanityInstance: () => instance}))
|
|
22
|
+
vi.mock('../../hooks/auth/useLoginUrl')
|
|
23
|
+
vi.mock('../../hooks/auth/useOAuthAuthorize', () => ({useOAuthAuthorize: vi.fn(() => vi.fn())}))
|
|
24
|
+
vi.mock('../../hooks/auth/useVerifyOrgProjects')
|
|
25
|
+
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
26
|
+
useHandleAuthCallback: vi.fn(() => async () => {}),
|
|
27
|
+
}))
|
|
28
|
+
vi.mock('../../hooks/auth/useLogOut', () => ({useLogOut: vi.fn(() => async () => {})}))
|
|
29
|
+
vi.mock('../../hooks/comlink/useWindowConnection', () => ({
|
|
30
|
+
useWindowConnection: vi.fn(() => ({fetch: vi.fn()})),
|
|
31
|
+
}))
|
|
32
|
+
vi.mock('../utils', () => ({isInIframe: vi.fn(() => false)}))
|
|
33
|
+
|
|
34
|
+
/** Stands in for the app's own boundary that receives errors the SDK rethrows. */
|
|
35
|
+
class AppBoundary extends Component<{children: ReactNode}, {caught: boolean}> {
|
|
36
|
+
override state = {caught: false}
|
|
37
|
+
static getDerivedStateFromError() {
|
|
38
|
+
return {caught: true}
|
|
39
|
+
}
|
|
40
|
+
override render() {
|
|
41
|
+
return this.state.caught ? 'app boundary caught it' : this.props.children
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Thrower({error}: {error: Error}): never {
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('AuthBoundary error reporting', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.mocked(reportError).mockClear()
|
|
52
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('reports a plain error from a child and still rethrows it to the app boundary', () => {
|
|
56
|
+
const error = new TypeError('sdk bug')
|
|
57
|
+
render(
|
|
58
|
+
<AppBoundary>
|
|
59
|
+
<AuthBoundary>
|
|
60
|
+
<Thrower error={error} />
|
|
61
|
+
</AuthBoundary>
|
|
62
|
+
</AppBoundary>,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText('app boundary caught it')).toBeInTheDocument()
|
|
66
|
+
expect(reportError).toHaveBeenCalledWith(error, {errorBoundary: 'AuthBoundary'})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('does not report an auth error, which the boundary renders itself', () => {
|
|
70
|
+
render(
|
|
71
|
+
<AppBoundary>
|
|
72
|
+
<AuthBoundary>
|
|
73
|
+
<Thrower error={new AuthError(new Error('login failed'))} />
|
|
74
|
+
</AuthBoundary>
|
|
75
|
+
</AppBoundary>,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(reportError).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -13,6 +13,7 @@ import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
|
|
|
13
13
|
import {useSanityInstance} from '../../hooks/context/useSanityInstance'
|
|
14
14
|
import {ChunkLoadError} from '../errors/ChunkLoadError'
|
|
15
15
|
import {CorsErrorComponent} from '../errors/CorsErrorComponent'
|
|
16
|
+
import {reportBoundaryError} from '../errors/reportBoundaryError'
|
|
16
17
|
import {isInIframe} from '../utils'
|
|
17
18
|
import {AuthError} from './AuthError'
|
|
18
19
|
import {ConfigurationError} from './ConfigurationError'
|
|
@@ -34,6 +35,8 @@ if (isInIframe() && !document.querySelector('[data-sanity-core]')) {
|
|
|
34
35
|
document.head.appendChild(script)
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const reportAuthBoundaryError = reportBoundaryError('AuthBoundary')
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* @internal
|
|
39
42
|
*/
|
|
@@ -124,6 +127,9 @@ export function AuthBoundary({
|
|
|
124
127
|
|
|
125
128
|
const FallbackComponent = useMemo(() => {
|
|
126
129
|
return function LoginComponentWithLayoutProps(fallbackProps: FallbackProps) {
|
|
130
|
+
// `LoginError` rethrows errors it does not render, and React skips `onError` on a boundary
|
|
131
|
+
// whose fallback throws, so the report has to happen here
|
|
132
|
+
reportAuthBoundaryError(fallbackProps.error)
|
|
127
133
|
// Chunk-load errors from any lazy-loaded code beneath this boundary
|
|
128
134
|
// (typically the consumer's app) get the chunk-aware fallback instead
|
|
129
135
|
// of being misreported as auth errors.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {CorsOriginError} from '@sanity/client'
|
|
2
|
+
import {reportError} from '@sanity/sdk/_internal'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {AuthError} from '../auth/AuthError'
|
|
6
|
+
import {ConfigurationError} from '../auth/ConfigurationError'
|
|
7
|
+
import {reportBoundaryError} from './reportBoundaryError'
|
|
8
|
+
|
|
9
|
+
vi.mock('@sanity/sdk/_internal', () => ({reportError: vi.fn()}))
|
|
10
|
+
|
|
11
|
+
describe('reportBoundaryError', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.mocked(reportError).mockClear()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('reports an unexpected error with the boundary name', () => {
|
|
17
|
+
const error = new TypeError('boom')
|
|
18
|
+
reportBoundaryError('AuthBoundary')(error)
|
|
19
|
+
expect(reportError).toHaveBeenCalledWith(error, {errorBoundary: 'AuthBoundary'})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it.each([
|
|
23
|
+
['AuthError', new AuthError(new Error('login failed'))],
|
|
24
|
+
['ConfigurationError', new ConfigurationError({message: 'bad config'})],
|
|
25
|
+
['CorsOriginError', new CorsOriginError({projectId: 'abc'})],
|
|
26
|
+
[
|
|
27
|
+
'a CorsOriginError from another copy of @sanity/client',
|
|
28
|
+
Object.assign(new Error('CorsOriginError'), {name: 'CorsOriginError'}),
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
'a chunk load error',
|
|
32
|
+
Object.assign(new Error('Loading chunk 5 failed.'), {name: 'ChunkLoadError'}),
|
|
33
|
+
],
|
|
34
|
+
])('does not report %s', (_name, error) => {
|
|
35
|
+
reportBoundaryError('AuthBoundary')(error)
|
|
36
|
+
expect(reportError).not.toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {CorsOriginError} from '@sanity/client'
|
|
2
|
+
import {isImportError} from '@sanity/sdk'
|
|
3
|
+
import {reportError} from '@sanity/sdk/_internal'
|
|
4
|
+
|
|
5
|
+
import {AuthError} from '../auth/AuthError'
|
|
6
|
+
import {ConfigurationError} from '../auth/ConfigurationError'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a function that reports an error an `ErrorBoundary` caught to Sentry. Call it from the
|
|
10
|
+
* boundary's `FallbackComponent`, not from `onError`: the SDK fallbacks rethrow errors they do not
|
|
11
|
+
* render, and React skips `onError` on a boundary whose fallback throws. Fallbacks render more
|
|
12
|
+
* than once for one error, which the reporter's deduplication absorbs.
|
|
13
|
+
*
|
|
14
|
+
* The boundaries also catch errors thrown by the app rendered beneath them, so the reporter keeps
|
|
15
|
+
* only errors whose throwing frame is in the SDK. Auth, configuration, CORS, and chunk load errors
|
|
16
|
+
* are expected states and are never reported.
|
|
17
|
+
*/
|
|
18
|
+
export function reportBoundaryError(errorBoundary: string) {
|
|
19
|
+
return (error: unknown): void => {
|
|
20
|
+
if (
|
|
21
|
+
error instanceof AuthError ||
|
|
22
|
+
error instanceof ConfigurationError ||
|
|
23
|
+
// The module bundle can hold a second copy of `@sanity/client`, so `instanceof` alone can
|
|
24
|
+
// miss an error thrown by the other copy
|
|
25
|
+
error instanceof CorsOriginError ||
|
|
26
|
+
(error instanceof Error && error.name === 'CorsOriginError') ||
|
|
27
|
+
isImportError(error)
|
|
28
|
+
) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
reportError(error, {errorBoundary})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {type AppFunctionContract} from '@sanity/sdk/_internal'
|
|
2
|
+
import {createContext} from 'react'
|
|
3
|
+
|
|
4
|
+
/** Function stack binding supplied by the CLI-generated app entry. @internal */
|
|
5
|
+
export interface AppFunctionsConfig {
|
|
6
|
+
/** Generated client contracts, supplied by the CLI. */
|
|
7
|
+
functions?: Record<string, AppFunctionContract>
|
|
8
|
+
stackId: string
|
|
9
|
+
organizationId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** App-local defaults for authenticated function clients. @internal */
|
|
13
|
+
export const AppFunctionsContext = createContext<AppFunctionsConfig | undefined>(undefined)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {type ReactNode} from 'react'
|
|
2
|
+
|
|
3
|
+
import {type AppFunctionsConfig, AppFunctionsContext} from './AppFunctionsContext'
|
|
4
|
+
|
|
5
|
+
export type {AppFunctionsConfig} from './AppFunctionsContext'
|
|
6
|
+
|
|
7
|
+
/** Props for the CLI's function binding provider. @internal */
|
|
8
|
+
export interface AppFunctionsProviderProps {
|
|
9
|
+
value: AppFunctionsConfig
|
|
10
|
+
children: ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provide a function stack to useClient within this application's React tree.
|
|
15
|
+
* Authentication stays with the SDK instance. The CLI supplies stack IDs and contracts.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export function AppFunctionsProvider({value, children}: AppFunctionsProviderProps): ReactNode {
|
|
19
|
+
return <AppFunctionsContext.Provider value={value}>{children}</AppFunctionsContext.Provider>
|
|
20
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {type ClientOptions, getClientState} from '@sanity/sdk'
|
|
3
|
+
import {useContext} from 'react'
|
|
2
4
|
|
|
5
|
+
import {AppFunctionsContext} from '../../context/AppFunctionsContext'
|
|
3
6
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
4
7
|
|
|
8
|
+
const useClientState = createStateSourceHook({
|
|
9
|
+
getState: getClientState,
|
|
10
|
+
getConfig: (options: ClientOptions) => options,
|
|
11
|
+
})
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* A React hook that provides a client that subscribes to changes in your application,
|
|
7
15
|
*
|
|
@@ -29,15 +37,21 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
|
29
37
|
* @public
|
|
30
38
|
* @function
|
|
31
39
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
export function useClient(options: ClientOptions): SanityClient {
|
|
41
|
+
const binding = useContext(AppFunctionsContext)
|
|
42
|
+
if (!options || typeof options !== 'object') {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'useClient() requires a configuration object with at least an "apiVersion" property. ' +
|
|
45
|
+
'Example: useClient({ apiVersion: "2024-11-12" })',
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return useClientState(
|
|
49
|
+
binding
|
|
50
|
+
? {
|
|
51
|
+
...options,
|
|
52
|
+
stackId: options.stackId ?? binding.stackId,
|
|
53
|
+
organizationId: options.organizationId ?? binding.organizationId,
|
|
54
|
+
}
|
|
55
|
+
: options,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {type AppFunctions} from '@sanity/sdk'
|
|
2
|
+
import {createAppFunctions} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useContext, useMemo} from 'react'
|
|
4
|
+
|
|
5
|
+
import {AppFunctionsContext} from '../../context/AppFunctionsContext'
|
|
6
|
+
import {useClient} from '../client/useClient'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Return the app's typed, authenticated functions. Types are generated by Sanity CLI.
|
|
10
|
+
* Each call waits for the function's result and validates it against the generated contract.
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const functions = useFunctions()
|
|
14
|
+
* const page = await functions.callWikipedia({timeout: 30_000})
|
|
15
|
+
* ```
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export function useFunctions(): AppFunctions {
|
|
19
|
+
const binding = useContext(AppFunctionsContext)
|
|
20
|
+
const client = useClient({apiVersion: '2026-06-15', scope: 'global'})
|
|
21
|
+
const contracts = binding?.functions
|
|
22
|
+
return useMemo(() => {
|
|
23
|
+
if (!contracts) {
|
|
24
|
+
throw new Error('App function contracts are missing. Build the app with Sanity CLI.')
|
|
25
|
+
}
|
|
26
|
+
return createAppFunctions(client, contracts)
|
|
27
|
+
}, [client, contracts])
|
|
28
|
+
}
|