@sanity/sdk-react 2.1.0 → 2.1.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.
- package/dist/index.d.ts +59 -0
- package/dist/index.js +52 -11
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/_exports/sdk-react.ts +2 -0
- package/src/components/SanityApp.test.tsx +42 -0
- package/src/components/SanityApp.tsx +3 -2
- package/src/context/ResourceProvider.test.tsx +9 -5
- package/src/hooks/dashboard/useManageFavorite.test.ts +2 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +2 -0
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +2 -0
- package/src/hooks/presence/usePresence.test.tsx +80 -0
- package/src/hooks/presence/usePresence.ts +23 -0
- package/src/hooks/users/useUser.test.tsx +387 -0
- package/src/hooks/users/useUser.ts +109 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getUsersState,
|
|
3
|
+
resolveUsers,
|
|
4
|
+
type SanityUser,
|
|
5
|
+
type StateSource,
|
|
6
|
+
type UserProfile,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {act, fireEvent, render, screen} from '@testing-library/react'
|
|
9
|
+
import {Suspense, useState} from 'react'
|
|
10
|
+
import {type Observable, Subject} from 'rxjs'
|
|
11
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
12
|
+
|
|
13
|
+
import {useUser} from './useUser'
|
|
14
|
+
|
|
15
|
+
// Mock the functions from '@sanity/sdk'
|
|
16
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
17
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
18
|
+
return {
|
|
19
|
+
...original,
|
|
20
|
+
getUsersState: vi.fn(),
|
|
21
|
+
resolveUsers: vi.fn(),
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Mock the Sanity instance hook to return a dummy instance
|
|
26
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
27
|
+
useSanityInstance: vi.fn().mockReturnValue({config: {projectId: 'p'}}),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
describe('useUser', () => {
|
|
31
|
+
// Create mock user profiles with all required fields
|
|
32
|
+
const mockUserProfile: UserProfile = {
|
|
33
|
+
id: 'profile1',
|
|
34
|
+
displayName: 'John Doe',
|
|
35
|
+
email: 'john.doe@example.com',
|
|
36
|
+
imageUrl: 'https://example.com/john.jpg',
|
|
37
|
+
provider: 'google',
|
|
38
|
+
createdAt: '2023-01-01T00:00:00Z',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mockUser: SanityUser = {
|
|
42
|
+
sanityUserId: 'gabc123',
|
|
43
|
+
profile: mockUserProfile,
|
|
44
|
+
memberships: [],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mockUserProfile2: UserProfile = {
|
|
48
|
+
id: 'profile2',
|
|
49
|
+
displayName: 'Jane Smith',
|
|
50
|
+
email: 'jane.smith@example.com',
|
|
51
|
+
imageUrl: 'https://example.com/jane.jpg',
|
|
52
|
+
provider: 'github',
|
|
53
|
+
createdAt: '2023-01-02T00:00:00Z',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const mockUser2: SanityUser = {
|
|
57
|
+
sanityUserId: 'gdef456',
|
|
58
|
+
profile: mockUserProfile2,
|
|
59
|
+
memberships: [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it('should render user data immediately when available', () => {
|
|
63
|
+
const getCurrent = vi.fn().mockReturnValue({
|
|
64
|
+
data: [mockUser],
|
|
65
|
+
hasMore: false,
|
|
66
|
+
totalCount: 1,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Type assertion to fix the StateSource type issue
|
|
70
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
71
|
+
getCurrent,
|
|
72
|
+
subscribe: vi.fn(),
|
|
73
|
+
get observable(): Observable<unknown> {
|
|
74
|
+
throw new Error('Not implemented')
|
|
75
|
+
},
|
|
76
|
+
} as unknown as StateSource<
|
|
77
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
78
|
+
>)
|
|
79
|
+
|
|
80
|
+
function TestComponent() {
|
|
81
|
+
const {data, isPending} = useUser({
|
|
82
|
+
userId: 'gabc123',
|
|
83
|
+
resourceType: 'organization',
|
|
84
|
+
organizationId: 'test-org',
|
|
85
|
+
})
|
|
86
|
+
return (
|
|
87
|
+
<div data-testid="output">
|
|
88
|
+
{data ? `${data.profile.displayName} (${data.sanityUserId})` : 'No user'} -{' '}
|
|
89
|
+
{isPending ? 'pending' : 'not pending'}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
render(<TestComponent />)
|
|
95
|
+
|
|
96
|
+
// Verify that the output contains the user data and that isPending is false
|
|
97
|
+
expect(screen.getByTestId('output').textContent).toContain('John Doe (gabc123)')
|
|
98
|
+
expect(screen.getByTestId('output').textContent).toContain('not pending')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should suspend rendering until user data is resolved via Suspense', async () => {
|
|
102
|
+
const ref = {
|
|
103
|
+
current: undefined as {data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined,
|
|
104
|
+
}
|
|
105
|
+
const getCurrent = vi.fn(() => ref.current)
|
|
106
|
+
const storeChanged$ = new Subject<void>()
|
|
107
|
+
|
|
108
|
+
// Type assertion to fix the StateSource type issue
|
|
109
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
110
|
+
getCurrent,
|
|
111
|
+
subscribe: vi.fn((cb) => {
|
|
112
|
+
const subscription = storeChanged$.subscribe(cb)
|
|
113
|
+
return () => subscription.unsubscribe()
|
|
114
|
+
}),
|
|
115
|
+
get observable(): Observable<unknown> {
|
|
116
|
+
throw new Error('Not implemented')
|
|
117
|
+
},
|
|
118
|
+
} as unknown as StateSource<
|
|
119
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
120
|
+
>)
|
|
121
|
+
|
|
122
|
+
// Create a controllable promise to simulate the user resolution
|
|
123
|
+
let resolvePromise: () => void
|
|
124
|
+
// Mock resolveUsers to return our fake promise
|
|
125
|
+
vi.mocked(resolveUsers).mockReturnValue(
|
|
126
|
+
new Promise<{data: SanityUser[]; totalCount: number; hasMore: boolean}>((resolve) => {
|
|
127
|
+
resolvePromise = () => {
|
|
128
|
+
ref.current = {
|
|
129
|
+
data: [mockUser],
|
|
130
|
+
hasMore: false,
|
|
131
|
+
totalCount: 1,
|
|
132
|
+
}
|
|
133
|
+
storeChanged$.next()
|
|
134
|
+
resolve(ref.current)
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
function TestComponent() {
|
|
140
|
+
const {data} = useUser({
|
|
141
|
+
userId: 'gabc123',
|
|
142
|
+
resourceType: 'organization',
|
|
143
|
+
organizationId: 'test-org',
|
|
144
|
+
})
|
|
145
|
+
return <div data-testid="output">{data?.profile.displayName || 'No user'}</div>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
render(
|
|
149
|
+
<Suspense fallback={<div data-testid="fallback">Loading...</div>}>
|
|
150
|
+
<TestComponent />
|
|
151
|
+
</Suspense>,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Initially, since storeValue is undefined, the component should suspend and fallback is shown
|
|
155
|
+
expect(screen.getByTestId('fallback')).toBeInTheDocument()
|
|
156
|
+
|
|
157
|
+
// Now simulate that data becomes available
|
|
158
|
+
await act(async () => {
|
|
159
|
+
resolvePromise()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
expect(screen.getByTestId('output').textContent).toContain('John Doe')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should display transition pending state during options change', async () => {
|
|
166
|
+
const ref = {
|
|
167
|
+
current: undefined as {data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined,
|
|
168
|
+
}
|
|
169
|
+
const getCurrent = vi.fn(() => ref.current)
|
|
170
|
+
const storeChanged$ = new Subject<void>()
|
|
171
|
+
|
|
172
|
+
// Use a more specific type for the mock implementation
|
|
173
|
+
vi.mocked(getUsersState).mockImplementation((_instance, options) => {
|
|
174
|
+
if (options?.userId === 'gabc123') {
|
|
175
|
+
return {
|
|
176
|
+
getCurrent: vi.fn().mockReturnValue({
|
|
177
|
+
data: [mockUser],
|
|
178
|
+
hasMore: false,
|
|
179
|
+
totalCount: 1,
|
|
180
|
+
}),
|
|
181
|
+
subscribe: vi.fn(),
|
|
182
|
+
get observable(): Observable<unknown> {
|
|
183
|
+
throw new Error('Not implemented')
|
|
184
|
+
},
|
|
185
|
+
} as unknown as StateSource<
|
|
186
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
187
|
+
>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
getCurrent,
|
|
192
|
+
subscribe: vi.fn((cb) => {
|
|
193
|
+
const subscription = storeChanged$.subscribe(cb)
|
|
194
|
+
return () => subscription.unsubscribe()
|
|
195
|
+
}),
|
|
196
|
+
get observable(): Observable<unknown> {
|
|
197
|
+
throw new Error('Not implemented')
|
|
198
|
+
},
|
|
199
|
+
} as unknown as StateSource<
|
|
200
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
201
|
+
>
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Create a controllable promise to simulate the user resolution
|
|
205
|
+
let resolvePromise: () => void
|
|
206
|
+
// Mock resolveUsers to return our fake promise
|
|
207
|
+
vi.mocked(resolveUsers).mockReturnValue(
|
|
208
|
+
new Promise<{data: SanityUser[]; totalCount: number; hasMore: boolean}>((resolve) => {
|
|
209
|
+
resolvePromise = () => {
|
|
210
|
+
ref.current = {
|
|
211
|
+
data: [mockUser2],
|
|
212
|
+
hasMore: false,
|
|
213
|
+
totalCount: 1,
|
|
214
|
+
}
|
|
215
|
+
storeChanged$.next()
|
|
216
|
+
resolve(ref.current)
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
function WrapperComponent() {
|
|
222
|
+
const [userId, setUserId] = useState('gabc123')
|
|
223
|
+
const {data, isPending} = useUser({
|
|
224
|
+
userId,
|
|
225
|
+
resourceType: 'organization',
|
|
226
|
+
organizationId: 'test-org',
|
|
227
|
+
})
|
|
228
|
+
return (
|
|
229
|
+
<div>
|
|
230
|
+
<div data-testid="output">
|
|
231
|
+
{data?.profile.displayName || 'No user'} - {isPending ? 'pending' : 'not pending'}
|
|
232
|
+
</div>
|
|
233
|
+
<button data-testid="button" onClick={() => setUserId('gdef456')}>
|
|
234
|
+
Change User
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
render(<WrapperComponent />)
|
|
241
|
+
|
|
242
|
+
// Initially, should show data for first user and not pending
|
|
243
|
+
expect(screen.getByTestId('output').textContent).toContain('John Doe')
|
|
244
|
+
expect(screen.getByTestId('output').textContent).toContain('not pending')
|
|
245
|
+
|
|
246
|
+
// Change the user ID, which should trigger a transition
|
|
247
|
+
fireEvent.click(screen.getByTestId('button'))
|
|
248
|
+
|
|
249
|
+
// The isPending should become true during the transition
|
|
250
|
+
expect(screen.getByTestId('output').textContent).toContain('pending')
|
|
251
|
+
|
|
252
|
+
// Resolve the promise to complete the transition
|
|
253
|
+
await act(async () => {
|
|
254
|
+
resolvePromise()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// After transition, should show new user data and not pending
|
|
258
|
+
expect(screen.getByTestId('output').textContent).toContain('Jane Smith')
|
|
259
|
+
expect(screen.getByTestId('output').textContent).toContain('not pending')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should handle undefined user data gracefully', () => {
|
|
263
|
+
const getCurrent = vi.fn().mockReturnValue({
|
|
264
|
+
data: [], // Empty array means no user found
|
|
265
|
+
hasMore: false,
|
|
266
|
+
totalCount: 0,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
270
|
+
getCurrent,
|
|
271
|
+
subscribe: vi.fn(),
|
|
272
|
+
get observable(): Observable<unknown> {
|
|
273
|
+
throw new Error('Not implemented')
|
|
274
|
+
},
|
|
275
|
+
} as unknown as StateSource<
|
|
276
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
277
|
+
>)
|
|
278
|
+
|
|
279
|
+
function TestComponent() {
|
|
280
|
+
const {data, isPending} = useUser({
|
|
281
|
+
userId: 'nonexistent123',
|
|
282
|
+
resourceType: 'organization',
|
|
283
|
+
organizationId: 'test-org',
|
|
284
|
+
})
|
|
285
|
+
return (
|
|
286
|
+
<div data-testid="output">
|
|
287
|
+
{data ? `Found: ${data.profile.displayName}` : 'User not found'} -{' '}
|
|
288
|
+
{isPending ? 'pending' : 'not pending'}
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
render(<TestComponent />)
|
|
294
|
+
|
|
295
|
+
expect(screen.getByTestId('output').textContent).toContain('User not found')
|
|
296
|
+
expect(screen.getByTestId('output').textContent).toContain('not pending')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should work with project-scoped user IDs', () => {
|
|
300
|
+
const projectUser: SanityUser = {
|
|
301
|
+
...mockUser,
|
|
302
|
+
sanityUserId: 'p12345',
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const getCurrent = vi.fn().mockReturnValue({
|
|
306
|
+
data: [projectUser],
|
|
307
|
+
hasMore: false,
|
|
308
|
+
totalCount: 1,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
vi.mocked(getUsersState).mockReturnValue({
|
|
312
|
+
getCurrent,
|
|
313
|
+
subscribe: vi.fn(),
|
|
314
|
+
get observable(): Observable<unknown> {
|
|
315
|
+
throw new Error('Not implemented')
|
|
316
|
+
},
|
|
317
|
+
} as unknown as StateSource<
|
|
318
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
319
|
+
>)
|
|
320
|
+
|
|
321
|
+
function TestComponent() {
|
|
322
|
+
const {data} = useUser({
|
|
323
|
+
userId: 'p12345',
|
|
324
|
+
resourceType: 'project',
|
|
325
|
+
projectId: 'test-project',
|
|
326
|
+
})
|
|
327
|
+
return (
|
|
328
|
+
<div data-testid="output">
|
|
329
|
+
{data ? `${data.profile.displayName} (${data.sanityUserId})` : 'No user'}
|
|
330
|
+
</div>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
render(<TestComponent />)
|
|
335
|
+
|
|
336
|
+
expect(screen.getByTestId('output').textContent).toContain('John Doe (p12345)')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should handle resource type changes', () => {
|
|
340
|
+
vi.mocked(getUsersState).mockImplementation((_instance, options) => {
|
|
341
|
+
const mockData = options?.resourceType === 'project' ? [mockUser] : [mockUser2]
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
getCurrent: vi.fn().mockReturnValue({
|
|
345
|
+
data: mockData,
|
|
346
|
+
hasMore: false,
|
|
347
|
+
totalCount: 1,
|
|
348
|
+
}),
|
|
349
|
+
subscribe: vi.fn(),
|
|
350
|
+
get observable(): Observable<unknown> {
|
|
351
|
+
throw new Error('Not implemented')
|
|
352
|
+
},
|
|
353
|
+
} as unknown as StateSource<
|
|
354
|
+
{data: SanityUser[]; totalCount: number; hasMore: boolean} | undefined
|
|
355
|
+
>
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
function WrapperComponent() {
|
|
359
|
+
const [resourceType, setResourceType] = useState<'project' | 'organization'>('project')
|
|
360
|
+
const {data} = useUser({
|
|
361
|
+
userId: 'gabc123',
|
|
362
|
+
resourceType,
|
|
363
|
+
projectId: resourceType === 'project' ? 'test-project' : undefined,
|
|
364
|
+
organizationId: resourceType === 'organization' ? 'test-org' : undefined,
|
|
365
|
+
})
|
|
366
|
+
return (
|
|
367
|
+
<div>
|
|
368
|
+
<div data-testid="output">{data?.profile.displayName || 'No user'}</div>
|
|
369
|
+
<button data-testid="toggle" onClick={() => setResourceType('organization')}>
|
|
370
|
+
Switch to Organization
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
render(<WrapperComponent />)
|
|
377
|
+
|
|
378
|
+
// Initially should show project user
|
|
379
|
+
expect(screen.getByTestId('output').textContent).toContain('John Doe')
|
|
380
|
+
|
|
381
|
+
// Switch resource type
|
|
382
|
+
fireEvent.click(screen.getByTestId('toggle'))
|
|
383
|
+
|
|
384
|
+
// Should now show organization user
|
|
385
|
+
expect(screen.getByTestId('output').textContent).toContain('Jane Smith')
|
|
386
|
+
})
|
|
387
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type GetUserOptions,
|
|
3
|
+
getUsersKey,
|
|
4
|
+
getUsersState,
|
|
5
|
+
parseUsersKey,
|
|
6
|
+
resolveUsers,
|
|
7
|
+
type SanityUser,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
|
+
|
|
11
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
* @category Types
|
|
16
|
+
*/
|
|
17
|
+
export interface UserResult {
|
|
18
|
+
/**
|
|
19
|
+
* The user data fetched, or undefined if not found.
|
|
20
|
+
*/
|
|
21
|
+
data: SanityUser | undefined
|
|
22
|
+
/**
|
|
23
|
+
* Whether a user request is currently in progress
|
|
24
|
+
*/
|
|
25
|
+
isPending: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*
|
|
32
|
+
* Retrieves a single user by ID for a given resource (either a project or an organization).
|
|
33
|
+
*
|
|
34
|
+
* @category Users
|
|
35
|
+
* @param options - The user ID, resource type, project ID, or organization ID
|
|
36
|
+
* @returns The user data and loading state
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```
|
|
40
|
+
* const { data, isPending } = useUser({
|
|
41
|
+
* userId: 'gabc123',
|
|
42
|
+
* resourceType: 'project',
|
|
43
|
+
* projectId: 'my-project-id',
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* return (
|
|
47
|
+
* <div>
|
|
48
|
+
* {isPending && <p>Loading...</p>}
|
|
49
|
+
* {data && (
|
|
50
|
+
* <figure>
|
|
51
|
+
* <img src={data.profile.imageUrl} alt='' />
|
|
52
|
+
* <figcaption>{data.profile.displayName}</figcaption>
|
|
53
|
+
* <address>{data.profile.email}</address>
|
|
54
|
+
* </figure>
|
|
55
|
+
* )}
|
|
56
|
+
* </div>
|
|
57
|
+
* )
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function useUser(options: GetUserOptions): UserResult {
|
|
61
|
+
const instance = useSanityInstance(options)
|
|
62
|
+
// Use React's useTransition to avoid UI jank when user options change
|
|
63
|
+
const [isPending, startTransition] = useTransition()
|
|
64
|
+
|
|
65
|
+
// Get the unique key for this user request and its options
|
|
66
|
+
const key = getUsersKey(instance, options)
|
|
67
|
+
// Use a deferred state to avoid immediate re-renders when the user request changes
|
|
68
|
+
const [deferredKey, setDeferredKey] = useState(key)
|
|
69
|
+
// Parse the deferred user key back into user options
|
|
70
|
+
const deferred = useMemo(() => parseUsersKey(deferredKey), [deferredKey])
|
|
71
|
+
|
|
72
|
+
// Create an AbortController to cancel in-flight requests when needed
|
|
73
|
+
const [ref, setRef] = useState<AbortController>(new AbortController())
|
|
74
|
+
|
|
75
|
+
// When the user request or options change, start a transition to update the request
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (key === deferredKey) return
|
|
78
|
+
|
|
79
|
+
startTransition(() => {
|
|
80
|
+
if (!ref.signal.aborted) {
|
|
81
|
+
ref.abort()
|
|
82
|
+
setRef(new AbortController())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setDeferredKey(key)
|
|
86
|
+
})
|
|
87
|
+
}, [deferredKey, key, ref])
|
|
88
|
+
|
|
89
|
+
// Get the state source for this user request from the users store
|
|
90
|
+
// We pass the userId as part of options to getUsersState
|
|
91
|
+
const {getCurrent, subscribe} = useMemo(() => {
|
|
92
|
+
return getUsersState(instance, deferred as GetUserOptions)
|
|
93
|
+
}, [instance, deferred])
|
|
94
|
+
|
|
95
|
+
// If data isn't available yet, suspend rendering until it is
|
|
96
|
+
// This is the React Suspense integration - throwing a promise
|
|
97
|
+
// will cause React to show the nearest Suspense fallback
|
|
98
|
+
if (getCurrent() === undefined) {
|
|
99
|
+
throw resolveUsers(instance, {...(deferred as GetUserOptions), signal: ref.signal})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Subscribe to updates and get the current data
|
|
103
|
+
// useSyncExternalStore ensures the component re-renders when the data changes
|
|
104
|
+
// Extract the first user from the users array (since we're fetching by userId, there should be only one)
|
|
105
|
+
const result = useSyncExternalStore(subscribe, getCurrent)
|
|
106
|
+
const data = result?.data[0]
|
|
107
|
+
|
|
108
|
+
return {data, isPending}
|
|
109
|
+
}
|