@sanity/sdk-react 0.0.0-alpha.1

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 (60) hide show
  1. package/dist/_chunks-es/useLogOut.js +36 -0
  2. package/dist/_chunks-es/useLogOut.js.map +1 -0
  3. package/dist/components.d.ts +235 -0
  4. package/dist/components.js +250 -0
  5. package/dist/components.js.map +1 -0
  6. package/dist/hooks.d.ts +145 -0
  7. package/dist/hooks.js +27 -0
  8. package/dist/hooks.js.map +1 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -0
  12. package/package.json +113 -0
  13. package/src/_exports/components.ts +12 -0
  14. package/src/_exports/hooks.ts +7 -0
  15. package/src/_exports/index.ts +10 -0
  16. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +95 -0
  17. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +42 -0
  18. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +23 -0
  19. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +95 -0
  20. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +42 -0
  21. package/src/components/DocumentListLayout/DocumentListLayout.tsx +15 -0
  22. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +49 -0
  23. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +34 -0
  24. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +30 -0
  25. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +115 -0
  26. package/src/components/Login/LoginLinks.test.tsx +100 -0
  27. package/src/components/Login/LoginLinks.tsx +73 -0
  28. package/src/components/auth/AuthBoundary.test.tsx +103 -0
  29. package/src/components/auth/AuthBoundary.tsx +101 -0
  30. package/src/components/auth/AuthError.test.ts +36 -0
  31. package/src/components/auth/AuthError.ts +27 -0
  32. package/src/components/auth/Login.test.tsx +41 -0
  33. package/src/components/auth/Login.tsx +58 -0
  34. package/src/components/auth/LoginCallback.test.tsx +86 -0
  35. package/src/components/auth/LoginCallback.tsx +41 -0
  36. package/src/components/auth/LoginError.test.tsx +56 -0
  37. package/src/components/auth/LoginError.tsx +54 -0
  38. package/src/components/auth/LoginFooter.test.tsx +29 -0
  39. package/src/components/auth/LoginFooter.tsx +67 -0
  40. package/src/components/auth/LoginLayout.test.tsx +33 -0
  41. package/src/components/auth/LoginLayout.tsx +99 -0
  42. package/src/components/context/SanityProvider.test.tsx +25 -0
  43. package/src/components/context/SanityProvider.tsx +42 -0
  44. package/src/hooks/Documents/.keep +0 -0
  45. package/src/hooks/auth/useAuthState.test.tsx +106 -0
  46. package/src/hooks/auth/useAuthState.tsx +33 -0
  47. package/src/hooks/auth/useAuthToken.test.tsx +94 -0
  48. package/src/hooks/auth/useAuthToken.tsx +16 -0
  49. package/src/hooks/auth/useCurrentUser.test.tsx +50 -0
  50. package/src/hooks/auth/useCurrentUser.tsx +27 -0
  51. package/src/hooks/auth/useHandleCallback.test.tsx +25 -0
  52. package/src/hooks/auth/useHandleCallback.tsx +50 -0
  53. package/src/hooks/auth/useLogOut.test.tsx +67 -0
  54. package/src/hooks/auth/useLogOut.tsx +15 -0
  55. package/src/hooks/auth/useLoginUrls.test.tsx +61 -0
  56. package/src/hooks/auth/useLoginUrls.tsx +51 -0
  57. package/src/hooks/client/useClient.test.tsx +130 -0
  58. package/src/hooks/client/useClient.ts +56 -0
  59. package/src/hooks/context/useSanityInstance.test.tsx +31 -0
  60. package/src/hooks/context/useSanityInstance.ts +23 -0
@@ -0,0 +1,130 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {act} from '@testing-library/react'
3
+ import type {Subscribable, Subscriber} from 'rxjs'
4
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
5
+
6
+ import {renderHook} from '../../../test/test-utils'
7
+ import {useClient} from './useClient'
8
+
9
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
10
+ const actual = await importOriginal()
11
+ return {
12
+ ...actual,
13
+ getClient: vi.fn(),
14
+ getSubscribableClient: vi.fn(),
15
+ }
16
+ })
17
+
18
+ const {getClient, getSubscribableClient} = await import('@sanity/sdk')
19
+
20
+ describe('useClient', () => {
21
+ let subscribers: {next: (client: SanityClient) => void}[] = []
22
+ let currentClient: SanityClient
23
+
24
+ beforeEach(() => {
25
+ subscribers = []
26
+
27
+ currentClient = {
28
+ config: () => ({token: undefined, apiVersion: 'v2024-11-12'}),
29
+ } as unknown as SanityClient
30
+
31
+ // Create a subscribable interface directly
32
+ const createSubscribable = (): Subscribable<SanityClient> => ({
33
+ subscribe: (subscriber: {next: (client: SanityClient) => void}) => {
34
+ subscribers.push(subscriber)
35
+ subscriber.next(currentClient)
36
+ return {
37
+ unsubscribe: () => {
38
+ const index = subscribers.indexOf(subscriber)
39
+ if (index > -1) subscribers.splice(index, 1)
40
+ },
41
+ }
42
+ },
43
+ })
44
+
45
+ vi.mocked(getClient).mockReturnValue(currentClient)
46
+ vi.mocked(getSubscribableClient).mockImplementation(() => createSubscribable())
47
+ })
48
+ it('should return initial client', () => {
49
+ const {result} = renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
50
+
51
+ expect(result.current.config().token).toBeUndefined()
52
+ expect(result.current.config().apiVersion).toBe('v2024-11-12')
53
+ })
54
+
55
+ it('should handle client update through authentication changes', async () => {
56
+ let clientSubscriber: Subscriber<SanityClient> | undefined
57
+
58
+ // Create a subscribable that can simulate updates
59
+ vi.mocked(getSubscribableClient).mockImplementation(() => ({
60
+ subscribe: (subscriber: Subscriber<SanityClient>) => {
61
+ clientSubscriber = subscriber
62
+ // Send initial client
63
+ subscriber.next(currentClient)
64
+ return {
65
+ unsubscribe: vi.fn(),
66
+ }
67
+ },
68
+ }))
69
+
70
+ const {result} = renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
71
+
72
+ // Verify initial state
73
+ expect(result.current.config().token).toBeUndefined()
74
+ expect(clientSubscriber).toBeDefined()
75
+
76
+ // Create authenticated client
77
+ const authenticatedClient = {
78
+ config: () => ({token: 'auth-token', apiVersion: 'v2024-11-12'}),
79
+ } as unknown as SanityClient
80
+
81
+ // Update getClient to return the new client
82
+ vi.mocked(getClient).mockReturnValue(authenticatedClient)
83
+
84
+ // Simulate the client update that would happen after auth change
85
+ await act(async () => {
86
+ clientSubscriber!.next(authenticatedClient)
87
+ })
88
+
89
+ // Verify the client was updated with the new token
90
+ expect(result.current.config().token).toBe('auth-token')
91
+ })
92
+
93
+ it('should unsubscribe on unmount', () => {
94
+ const unsubscribeSpy = vi.fn()
95
+ vi.mocked(getSubscribableClient).mockImplementation(() => ({
96
+ subscribe: () => ({
97
+ unsubscribe: unsubscribeSpy,
98
+ }),
99
+ }))
100
+
101
+ const {unmount} = renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
102
+
103
+ unmount()
104
+ expect(unsubscribeSpy).toHaveBeenCalled()
105
+ })
106
+
107
+ it('should handle subscription errors', () => {
108
+ vi.spyOn(console, 'error').mockImplementation(() => {})
109
+
110
+ const testError = new Error('Subscription error')
111
+ let errorSubscriber: Subscriber<SanityClient> | undefined
112
+
113
+ // Mock getSubscribableClient to create a subscription that will error
114
+ vi.mocked(getSubscribableClient).mockImplementation(() => ({
115
+ subscribe: (subscriber: Subscriber<SanityClient>) => {
116
+ errorSubscriber = subscriber
117
+ return {
118
+ unsubscribe: vi.fn(),
119
+ }
120
+ },
121
+ }))
122
+
123
+ renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
124
+
125
+ errorSubscriber!.error(testError)
126
+
127
+ // eslint-disable-next-line no-console
128
+ expect(console.error).toHaveBeenCalledWith('Error in useClient subscription:', testError)
129
+ })
130
+ })
@@ -0,0 +1,56 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {type ClientOptions, getClient, getSubscribableClient} from '@sanity/sdk'
3
+ import {useCallback, useSyncExternalStore} from 'react'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+
7
+ /**
8
+ * A React hook that provides a client that subscribes to changes in your application,
9
+ * such as user authentication changes.
10
+ *
11
+ * @remarks
12
+ * The hook uses `useSyncExternalStore` to safely subscribe to changes
13
+ * and ensure consistency between server and client rendering.
14
+ *
15
+ * @returns A Sanity client
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * function MyComponent() {
20
+ * const client = useClient()
21
+ * const [document, setDocument] = useState(null)
22
+ * useEffect(async () => {
23
+ * const doc = client.fetch('*[_id == "myDocumentId"]')
24
+ * setDocument(doc)
25
+ * }, [])
26
+ * return <div>{document ?? 'Loading...'}</div>
27
+ * }
28
+ * ```
29
+ *
30
+ * @public
31
+ */
32
+ export function useClient(options: ClientOptions): SanityClient {
33
+ const instance = useSanityInstance()
34
+
35
+ const subscribe = useCallback(
36
+ (onStoreChange: () => void) => {
37
+ const client$ = getSubscribableClient(options, instance)
38
+ const subscription = client$.subscribe({
39
+ next: onStoreChange,
40
+ error: (error) => {
41
+ // @TODO: We should tackle error handling / error boundaries soon
42
+ // eslint-disable-next-line no-console
43
+ console.error('Error in useClient subscription:', error)
44
+ },
45
+ })
46
+ return () => subscription.unsubscribe()
47
+ },
48
+ [instance, options],
49
+ )
50
+
51
+ const getSnapshot = useCallback(() => {
52
+ return getClient(options, instance)
53
+ }, [instance, options])
54
+
55
+ return useSyncExternalStore(subscribe, getSnapshot)
56
+ }
@@ -0,0 +1,31 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import React from 'react'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {SanityProvider} from '../../components/context/SanityProvider'
7
+ import {useSanityInstance} from './useSanityInstance'
8
+
9
+ describe('useSanityInstance', () => {
10
+ const sanityInstance = createSanityInstance({projectId: 'test-project', dataset: 'production'})
11
+
12
+ it('returns sanity instance when used within provider', () => {
13
+ const wrapper = ({children}: {children: React.ReactNode}) => (
14
+ <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
15
+ )
16
+
17
+ const {result} = renderHook(() => useSanityInstance(), {wrapper})
18
+
19
+ expect(result.current).toBe(sanityInstance)
20
+ })
21
+
22
+ it('throws error when used outside provider', () => {
23
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
24
+
25
+ expect(() => {
26
+ renderHook(() => useSanityInstance())
27
+ }).toThrow('useSanityInstance must be called from within the SanityProvider')
28
+
29
+ consoleSpy.mockRestore()
30
+ })
31
+ })
@@ -0,0 +1,23 @@
1
+ import type {SanityInstance} from '@sanity/sdk'
2
+ import {useContext} from 'react'
3
+
4
+ import {SanityInstanceContext} from '../../components/context/SanityProvider'
5
+
6
+ /**
7
+ * Hook that provides the current Sanity instance from the context.
8
+ * This must be called from within a `SanityProvider` component.
9
+ * @public
10
+ * @returns the current Sanity instance
11
+ * @example
12
+ * ```tsx
13
+ * const instance = useSanityInstance()
14
+ * ```
15
+ */
16
+ export const useSanityInstance = (): SanityInstance => {
17
+ const sanityInstance = useContext(SanityInstanceContext)
18
+ if (!sanityInstance) {
19
+ throw new Error('useSanityInstance must be called from within the SanityProvider')
20
+ }
21
+
22
+ return sanityInstance
23
+ }