@sanity/sdk-react 2.4.0 → 2.6.0

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 (30) hide show
  1. package/README.md +652 -4
  2. package/dist/index.d.ts +85 -14
  3. package/dist/index.js +184 -57
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -8
  6. package/src/_exports/sdk-react.ts +5 -0
  7. package/src/components/SDKProvider.tsx +8 -3
  8. package/src/components/SanityApp.tsx +2 -1
  9. package/src/context/SourcesContext.tsx +7 -0
  10. package/src/context/renderSanityApp.test.tsx +355 -0
  11. package/src/context/renderSanityApp.tsx +48 -0
  12. package/src/hooks/agent/useAgentResourceContext.test.tsx +245 -0
  13. package/src/hooks/agent/useAgentResourceContext.ts +106 -0
  14. package/src/hooks/context/useSource.tsx +34 -0
  15. package/src/hooks/dashboard/useDispatchIntent.test.ts +25 -22
  16. package/src/hooks/dashboard/useDispatchIntent.ts +9 -10
  17. package/src/hooks/dashboard/utils/{getResourceIdFromDocumentHandle.test.ts → useResourceIdFromDocumentHandle.test.ts} +33 -59
  18. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +46 -0
  19. package/src/hooks/document/useApplyDocumentActions.test.ts +124 -9
  20. package/src/hooks/document/useApplyDocumentActions.ts +44 -4
  21. package/src/hooks/document/useDocumentPermissions.test.tsx +3 -3
  22. package/src/hooks/document/useDocumentPermissions.ts +9 -6
  23. package/src/hooks/document/useEditDocument.ts +3 -0
  24. package/src/hooks/documents/useDocuments.ts +3 -2
  25. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +1 -0
  26. package/src/hooks/query/useQuery.ts +21 -8
  27. package/src/hooks/releases/usePerspective.test.tsx +1 -0
  28. package/src/hooks/releases/usePerspective.ts +1 -1
  29. package/src/hooks/dashboard/types.ts +0 -12
  30. package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.ts +0 -53
package/README.md CHANGED
@@ -5,13 +5,661 @@
5
5
  <h1 align="center">Sanity App SDK (React)</h1>
6
6
  </p>
7
7
 
8
- React hooks for creating Sanity applications.
8
+ React hooks for creating Sanity applications. Live by default, optimistic updates, multi-project support.
9
+
10
+ ---
11
+
12
+ ## Quickstart
13
+
14
+ ### 1. Setup (2 min)
15
+
16
+ ```bash
17
+ npx sanity@latest init --template app-quickstart
18
+ cd your-app
19
+ npm run dev
20
+ ```
21
+
22
+ Opens at `https://www.sanity.io/welcome?dev=http%3A%2F%2Flocalhost%3A3333`, proxied through Sanity Dashboard for auth.
23
+
24
+ **Key files:**
25
+
26
+ - `sanity.cli.ts` — configuration options used by the CLI — application metadata, deployment config, etc
27
+ - `src/App.tsx` — Root with `<SanityApp>` provider and project configuration(s)
28
+ - `src/ExampleComponent.tsx` — Your starting point
29
+
30
+ ### 2. Project configuration
31
+
32
+ ```tsx
33
+ import {SanityApp, type SanityConfig} from '@sanity/sdk-react'
34
+
35
+ const config: SanityConfig[] = [
36
+ {projectId: 'abc123', dataset: 'production'},
37
+ {projectId: 'def456', dataset: 'production'}, // multi-project support
38
+ ]
39
+
40
+ export function App() {
41
+ return (
42
+ <SanityApp config={config} fallback={<div>Loading...</div>}>
43
+ <YourApp />
44
+ </SanityApp>
45
+ )
46
+ }
47
+ ```
48
+
49
+ **Auth is automatic** — Dashboard injects an auth token via iframe. No custom login flow is needed for your application.
50
+
51
+ ---
52
+
53
+ ## Guide
54
+
55
+ ### Document Handles
56
+
57
+ Document handles are a core concept for apps built with the App SDK. Document handles are minimal pointers to documents. They consist of the following properties:
58
+
59
+ ```tsx
60
+ type DocumentHandle = {
61
+ documentId: string
62
+ documentType: string
63
+ projectId?: string // optional if using the default projectId or inside a ResourceProvider
64
+ dataset?: string // optional if using the default dataset or inside a ResourceProvider
65
+ }
66
+ ```
67
+
68
+ **Best practice:** Fetch document handles first → pass them to child components → fetch individual document content from child components.
69
+
70
+ ---
71
+
72
+ ### Hook Reference
73
+
74
+ #### Data Retrieval
75
+
76
+ ```tsx
77
+ // Get a collection of document handles (structured for infinite scrolling)
78
+ const {data, hasMore, loadMore, isPending, count} = useDocuments({
79
+ documentType: 'article',
80
+ batchSize: 20,
81
+ orderings: [{field: '_updatedAt', direction: 'desc'}],
82
+ filter: 'status == $status', // GROQ filter
83
+ params: {status: 'published'}, // Parameters used for the GROQ filter
84
+ })
85
+
86
+ // Get a collection of document handles (structured for paginated lists)
87
+ const {data, currentPage, totalPages, nextPage, previousPage} = usePaginatedDocuments({
88
+ documentType: 'article',
89
+ pageSize: 10,
90
+ })
91
+
92
+ // Get content from a single document (live content, optimistic updates when used with useEditDocument)
93
+ const {data: doc} = useDocument(handle)
94
+ const {data: title} = useDocument({...handle, path: 'title'})
95
+
96
+ // Get a projection for an individual document (live content, no optimistic updates)
97
+ const {data} = useDocumentProjection({
98
+ ...handle,
99
+ projection: `{ title, "author": author->name, "imageUrl": image.asset->url }`,
100
+ })
101
+
102
+ // Use GROQ directly
103
+ const {data} = useQuery({
104
+ query: `*[_type == "article" && featured == true][0...5]{ title, slug }`,
105
+ })
106
+ ```
107
+
108
+ #### Document Manipulation
109
+
110
+ ```tsx
111
+ // Edit field (emits optimistic updates to useEditDocument listeners, creates a draft automatically)
112
+ const editTitle = useEditDocument({...handle, path: 'title'})
113
+ editTitle('New Title') // fires on every keystroke, debounced internally
114
+
115
+ // Edit a nested path in a document
116
+ const editAuthorName = useEditDocument({...handle, path: 'author.name'})
117
+
118
+ // Document actions
119
+ import {
120
+ useApplyDocumentActions,
121
+ createDocumentHandle,
122
+ publishDocument,
123
+ unpublishDocument,
124
+ deleteDocument,
125
+ createDocument,
126
+ discardDraft,
127
+ } from '@sanity/sdk-react'
128
+
129
+ const apply = useApplyDocumentActions()
130
+
131
+ // Single action
132
+ await apply(publishDocument(handle))
133
+
134
+ // Batch actions
135
+ await apply([publishDocument(handle1), publishDocument(handle2), deleteDocument(handle3)])
136
+
137
+ // Create new document with an optional initial content
138
+ const newHandle = createDocumentHandle({
139
+ documentId: crypto.randomUUID(),
140
+ documentType: 'article',
141
+ })
142
+ await apply(createDocument(newHandle, {title: 'Untitled', status: 'draft'}))
143
+ ```
144
+
145
+ #### Events & Permissions
146
+
147
+ ```tsx
148
+ // Subscribe to document events
149
+ useDocumentEvent({
150
+ ...handle,
151
+ onEvent: (event) => {
152
+ // event.type: 'documentEdited' | 'documentPublished' | 'documentDeleted' | ...
153
+ console.log(event.type, event.documentId)
154
+ },
155
+ })
156
+
157
+ // Check permissions
158
+ const {data: canEdit} = useDocumentPermissions({
159
+ ...handle,
160
+ permission: 'update',
161
+ })
162
+ const {data: canPublish} = useDocumentPermissions({
163
+ ...handle,
164
+ permission: 'publish',
165
+ })
166
+ ```
167
+
168
+ ---
169
+
170
+ ### Document Actions
171
+
172
+ The `useApplyDocumentActions` hook is used to perform document lifecycle operations. Actions are created using helper functions and applied through the `apply` function.
173
+
174
+ #### Available Action Creators
175
+
176
+ | Function | Description |
177
+ | ------------------- | ---------------------------------------------- |
178
+ | `createDocument` | Create a new document |
179
+ | `publishDocument` | Publish a draft (copy draft → published) |
180
+ | `unpublishDocument` | Unpublish (delete published, keep draft) |
181
+ | `deleteDocument` | Delete document entirely (draft and published) |
182
+ | `discardDraft` | Discard draft changes, revert to published |
183
+
184
+ #### Creating Documents
185
+
186
+ To create a document, you must:
187
+
188
+ 1. Generate your own document ID (using `crypto.randomUUID()`)
189
+ 2. Create a document handle with `createDocumentHandle`
190
+ 3. Apply the `createDocument` action using the document handle, along with optional initial content
191
+
192
+ ```tsx
193
+ import {useApplyDocumentActions, createDocumentHandle, createDocument} from '@sanity/sdk-react'
194
+
195
+ function CreateArticleButton() {
196
+ const apply = useApplyDocumentActions()
197
+
198
+ const handleCreateArticle = () => {
199
+ const newId = crypto.randomUUID()
200
+ const handle = createDocumentHandle({
201
+ documentId: newId,
202
+ documentType: 'article',
203
+ })
204
+
205
+ apply(
206
+ createDocument(handle, {
207
+ title: 'New Article',
208
+ status: 'draft',
209
+ author: {_type: 'reference', _ref: 'author-123'},
210
+ }),
211
+ )
212
+
213
+ // Navigate to the new document
214
+ navigate(`/articles/${newId}`)
215
+ }
216
+
217
+ return <button onClick={handleCreateArticle}>Create Article</button>
218
+ }
219
+ ```
220
+
221
+ #### Publishing Documents
222
+
223
+ ```tsx
224
+ import {useApplyDocumentActions, publishDocument, useDocument} from '@sanity/sdk-react'
225
+
226
+ function PublishButton({handle}: {handle: DocumentHandle}) {
227
+ const apply = useApplyDocumentActions()
228
+ const {data: doc} = useDocument(handle)
229
+
230
+ // Check if document has unpublished changes (is a draft)
231
+ const isDraft = doc?._id?.startsWith('drafts.')
232
+
233
+ return (
234
+ <button disabled={!isDraft} onClick={() => apply(publishDocument(handle))}>
235
+ Publish
236
+ </button>
237
+ )
238
+ }
239
+ ```
240
+
241
+ #### Deleting Documents
242
+
243
+ ```tsx
244
+ import {useApplyDocumentActions, deleteDocument} from '@sanity/sdk-react'
245
+
246
+ function DeleteButton({handle}: {handle: DocumentHandle}) {
247
+ const apply = useApplyDocumentActions()
248
+
249
+ const handleDelete = () => {
250
+ if (confirm('Are you sure?')) {
251
+ apply(deleteDocument(handle))
252
+ }
253
+ }
254
+
255
+ return <button onClick={handleDelete}>Delete</button>
256
+ }
257
+ ```
258
+
259
+ #### Batch Operations
260
+
261
+ Apply multiple actions as a single transaction:
262
+
263
+ ```tsx
264
+ const apply = useApplyDocumentActions()
265
+
266
+ // Create and immediately publish
267
+ const newHandle = createDocumentHandle({
268
+ documentId: crypto.randomUUID(),
269
+ documentType: 'article',
270
+ })
271
+
272
+ apply([createDocument(newHandle, {title: 'Breaking News'}), publishDocument(newHandle)])
273
+
274
+ // Publish multiple documents at once
275
+ apply([publishDocument(handle1), publishDocument(handle2), publishDocument(handle3)])
276
+ ```
277
+
278
+ ---
279
+
280
+ ### Suspense Pattern
281
+
282
+ All hooks that get or write data use React Suspense. Wrap all your components that fetch data with a Suspense boundary to avoid unnecessary re-renders:
283
+
284
+ ```tsx
285
+ function App() {
286
+ return (
287
+ <Suspense fallback={<Skeleton />}>
288
+ <ArticleList />
289
+ </Suspense>
290
+ )
291
+ }
292
+
293
+ function ArticleList() {
294
+ const {data: articles} = useDocuments({documentType: 'article'})
295
+
296
+ return (
297
+ <ul>
298
+ {articles.map((handle) => (
299
+ {/* Wrap each list item in its own Suspense boundary to prevent full list re-renders when one item updates */}
300
+ <Suspense key={handle.documentId} fallback={<li>Loading...</li>}>
301
+ <ArticleItem handle={handle} />
302
+ </Suspense>
303
+ ))}
304
+ </ul>
305
+ )
306
+ }
307
+ ```
308
+
309
+ ---
310
+
311
+ ### Draft/Published Model
312
+
313
+ Sanity has two document states:
314
+
315
+ - **Published:** `_id: "abc123"` — live, public
316
+ - **Draft:** `_id: "drafts.abc123"` — working copy
317
+
318
+ The SDK handles updating the document state automatically:
319
+
320
+ - `useDocument()` returns draft if exists, else published
321
+ - `useEditDocument()` creates draft on first edit (automatic)
322
+ - `publishDocument()` copies draft → published, deletes draft
323
+ - `discardDraft()` deletes draft, reverts to published
324
+
325
+ #### LiveEdit Documents
326
+
327
+ For documents that don't need the draft/published workflow (such as settings, configuration, or real-time collaborative documents), you can use **liveEdit mode** by setting `liveEdit: true` in the document handle:
328
+
329
+ ```tsx
330
+ const settingsHandle: DocumentHandle = {
331
+ documentId: 'site-settings',
332
+ documentType: 'settings',
333
+ liveEdit: true, // Edits apply directly without creating a draft
334
+ }
335
+
336
+ // Edits are applied immediately to the published document
337
+ const editSettings = useEditDocument(settingsHandle)
338
+ ```
339
+
340
+ **When using liveEdit documents:**
341
+
342
+ - Drafts will not be created when the document is edited
343
+ - Edits will be applied directly to the published document
344
+ - `publishDocument()`, `unpublishDocument()`, and `discardDraft()` actions cannot be used (since liveEdit documents are always published and do not have drafts)
345
+
346
+ For more details, see the [Sanity documentation on liveEdit documents](https://www.sanity.io/docs/content-lake/drafts).
347
+
348
+ ---
349
+
350
+ ### Real-Time Behavior
351
+
352
+ #### Live by Default
353
+
354
+ - Document changes from other users appear instantly
355
+ - No polling, uses Sanity's listener API
356
+ - Optimistic updates for local edits appear before the server confirms the updates
357
+
358
+ #### Re-render Triggers
359
+
360
+ Any mutation to a subscribed document (even fields you don't display) will trigger a re-render. Use `useDocumentProjection()` for read-only displays to minimize re-renders.
361
+
362
+ ---
363
+
364
+ ### Multi-Project Access
365
+
366
+ The SDK supports accessing documents from multiple projects and datasets simultaneously. There are two main approaches:
367
+
368
+ #### Approach 1: Specify Project/Dataset Directly in the Handle
369
+
370
+ Pass `projectId` and `dataset` directly in document handles to fetch data from specific projects (note that any `projectId` and `dataset` pair you pass must be defined in your application’s array of [SanityConfig objects](https://www.sanity.io/docs/app-sdk/sdk-configuration#d95b8773097c)):
371
+
372
+ ```tsx
373
+ import {useDocument} from '@sanity/sdk-react'
374
+
375
+ function MultiProjectComponent() {
376
+ // Fetch from Project A
377
+ const {data: productA} = useDocument({
378
+ documentId: 'product-123',
379
+ documentType: 'product',
380
+ projectId: 'project-a',
381
+ dataset: 'production',
382
+ })
383
+
384
+ // Fetch from Project B
385
+ const {data: productB} = useDocument({
386
+ documentId: 'product-456',
387
+ documentType: 'product',
388
+ projectId: 'project-b',
389
+ dataset: 'staging',
390
+ })
391
+
392
+ return (
393
+ <div>
394
+ <h2>{productA?.title} (Project A)</h2>
395
+ <h2>{productB?.title} (Project B)</h2>
396
+ </div>
397
+ )
398
+ }
399
+ ```
400
+
401
+ #### Approach 2: Use ResourceProvider to Set Context
402
+
403
+ Wrap components in `ResourceProvider` to set default project/dataset values for all child components:
404
+
405
+ ```tsx
406
+ // App.tsx
407
+ import {ResourceProvider, useDocument, useSanityInstance} from '@sanity/sdk-react'
408
+
409
+ function ProductCard({productId}: {productId: string}) {
410
+ // Get the current project/dataset from context
411
+ const {config} = useSanityInstance()
412
+
413
+ // No need to specify projectId/dataset - inherited from ResourceProvider
414
+ const {data: product} = useDocument({
415
+ documentId: productId,
416
+ documentType: 'product',
417
+ })
418
+
419
+ return (
420
+ <div>
421
+ <h3>{product?.title}</h3>
422
+ <p>
423
+ From: {config.projectId}.{config.dataset}
424
+ </p>
425
+ </div>
426
+ )
427
+ }
428
+
429
+ export function MultiProjectApp() {
430
+ return (
431
+ <div>
432
+ {/* Products from Project A */}
433
+ <ResourceProvider projectId="project-a" dataset="production" fallback={<div>Loading...</div>}>
434
+ <h2>Project A Products</h2>
435
+ <ProductCard productId="product-123" />
436
+ <ProductCard productId="product-456" />
437
+ </ResourceProvider>
438
+
439
+ {/* Products from Project B */}
440
+ <ResourceProvider projectId="project-b" dataset="staging" fallback={<div>Loading...</div>}>
441
+ <h2>Project B Products</h2>
442
+ <ProductCard productId="product-789" />
443
+ </ResourceProvider>
444
+ </div>
445
+ )
446
+ }
447
+ ```
448
+
449
+ **Key Points:**
450
+
451
+ - When using hooks that take document handles as arguments (such useDocument, useEditDocument, useQuery, etc.), the document handles’ `projectId` and `dataset` values can be explicitly set to fetch documents from arbitrary projects and datasets
452
+ - The ResourceProvider component is used to create a project ID and dataset context that child components will inherit from; this can negate the need to specify the project ID and dataset values for document handles in hooks called by child components
453
+ - Use `useSanityInstance()` to access the context configuration for the current component: `const {config} = useSanityInstance()`
454
+ - You can nest ResourceProvider components to create component trees with different project/dataset configurations — but be aware that, when the project ID and dataset values for document handles are _not_ specified, the project ID and dataset from the closest ResourceProvider context will be used
455
+ - Regardless of the approach you use, the project IDs and dataset names you reference (whether in document handles or ResourceProviders) must be enumerated in your application’s [SanityConfig objects](https://www.sanity.io/docs/app-sdk/sdk-configuration#d95b8773097c)
456
+
457
+ ---
458
+
459
+ ### TypeScript & TypeGen
460
+
461
+ ```bash
462
+ # Generate types from your schema
463
+ npx sanity typegen generate
464
+ ```
465
+
466
+ ```tsx
467
+ import type {Article} from './sanity.types'
468
+
469
+ const {data} = useDocument<Article>(handle)
470
+ // data is typed as Article
471
+ ```
472
+
473
+ ---
474
+
475
+ ### Deployment
476
+
477
+ ```bash
478
+ npx sanity deploy
479
+ ```
480
+
481
+ Add the resulting app ID to the `deployment` section of your `sanity.config.ts` file: `{deployment: { appId: "appbc1234", ... } }`.
482
+
483
+ App appears in Sanity Dashboard alongside Studios. Requires `sanity.sdk.applications.deploy` permission.
484
+
485
+ ---
486
+
487
+ ### UI Options
488
+
489
+ SDK is headless. Common choices:
490
+
491
+ ```bash
492
+ # Sanity UI (matches Studio aesthetic)
493
+ npm install @sanity/ui @sanity/icons styled-components
494
+
495
+ # Tailwind
496
+ npm install tailwindcss @tailwindcss/vite
497
+ ```
498
+
499
+ #### Tailwind Setup
500
+
501
+ Tailwind requires a few extra steps since the App SDK uses Vite internally.
502
+
503
+ 1. **Install dependencies:**
504
+
505
+ ```bash
506
+ npm install tailwindcss @tailwindcss/vite
507
+ ```
508
+
509
+ 2. **Configure the Vite plugin in `sanity.cli.ts`:**
510
+
511
+ ```ts
512
+ import {defineCliConfig} from 'sanity/cli'
513
+
514
+ export default defineCliConfig({
515
+ app: {
516
+ organizationId: 'your-org-id',
517
+ entry: './src/App.tsx',
518
+ },
519
+ vite: async (viteConfig) => {
520
+ const {default: tailwindcss} = await import('@tailwindcss/vite')
521
+ return {
522
+ ...viteConfig,
523
+ plugins: [...viteConfig.plugins, tailwindcss()],
524
+ }
525
+ },
526
+ })
527
+ ```
528
+
529
+ 3. **Import Tailwind in your CSS (e.g., `src/App.css`):**
530
+
531
+ ```css
532
+ @import 'tailwindcss';
533
+ ```
534
+
535
+ 4. **Import the CSS in your app:**
536
+
537
+ ```tsx
538
+ // src/App.tsx
539
+ import './App.css'
540
+ ```
541
+
542
+ Now you can use Tailwind classes in your components.
543
+
544
+ #### Portable Text Editor
545
+
546
+ Use `@portabletext/plugin-sdk-value` to connect a Portable Text Editor with a Sanity document field. It provides two-way sync, real-time collaboration, and optimistic updates.
547
+
548
+ 1. **Install dependencies:**
549
+
550
+ ```bash
551
+ npm install @portabletext/editor @portabletext/plugin-sdk-value
552
+ ```
553
+
554
+ 2. **Use in a component:**
555
+
556
+ ```tsx
557
+ import {defineSchema, EditorProvider, PortableTextEditable} from '@portabletext/editor'
558
+ import {SDKValuePlugin} from '@portabletext/plugin-sdk-value'
559
+
560
+ function MyEditor({documentId}: {documentId: string}) {
561
+ return (
562
+ <EditorProvider initialConfig={{schemaDefinition: defineSchema({})}}>
563
+ <PortableTextEditable />
564
+ <SDKValuePlugin documentId={documentId} documentType="article" path="content" />
565
+ </EditorProvider>
566
+ )
567
+ }
568
+ ```
569
+
570
+ ##### SDKValuePlugin Props
571
+
572
+ | Prop | Type | Description |
573
+ | -------------- | ------------------- | ----------------------------------------- |
574
+ | `documentId` | `string` | The document ID |
575
+ | `documentType` | `string` | The document type |
576
+ | `path` | `string` | JSONMatch path to the Portable Text field |
577
+ | `dataset` | `string` (optional) | Dataset name if different from default |
578
+ | `projectId` | `string` (optional) | Project ID if different from default |
579
+
580
+ **The plugin handles:**
581
+
582
+ - Two-way sync between editor and document
583
+ - Real-time updates from other users
584
+ - Optimistic updates for smooth UX
585
+
586
+ ---
587
+
588
+ ### Common Patterns
589
+
590
+ #### Editable List Item
591
+
592
+ ```tsx
593
+ function EditableTitle({handle}: {handle: DocumentHandle}) {
594
+ const {data: title} = useDocument({...handle, path: 'title'})
595
+ const editTitle = useEditDocument({...handle, path: 'title'})
596
+
597
+ return <input value={title ?? ''} onChange={(e) => editTitle(e.target.value)} />
598
+ }
599
+ ```
600
+
601
+ #### Publish Button with Permission Check
602
+
603
+ ```tsx
604
+ function PublishButton({handle}: {handle: DocumentHandle}) {
605
+ const {data: canPublish} = useDocumentPermissions({
606
+ ...handle,
607
+ permission: 'publish',
608
+ })
609
+ const apply = useApplyDocumentActions()
610
+
611
+ if (!canPublish) return null
612
+
613
+ return <button onClick={() => apply(publishDocument(handle))}>Publish</button>
614
+ }
615
+ ```
616
+
617
+ #### Document Status Indicator
618
+
619
+ ```tsx
620
+ function DocStatus({handle}: {handle: DocumentHandle}) {
621
+ const {data: published} = useDocumentProjection({
622
+ documentId: handle.documentId, // without drafts. prefix
623
+ documentType: handle.documentType,
624
+ projection: '{ _updatedAt }',
625
+ })
626
+
627
+ const {data: draft} = useDocumentProjection({
628
+ documentId: `drafts.${handle.documentId}`,
629
+ documentType: handle.documentType,
630
+ projection: '{ _updatedAt }',
631
+ })
632
+
633
+ if (draft && published) return <span>Modified</span>
634
+ if (draft) return <span>Draft</span>
635
+ if (published) return <span>Published</span>
636
+ return <span>New</span>
637
+ }
638
+ ```
639
+
640
+ ---
641
+
642
+ ## Quick Reference
643
+
644
+ | Task | Hook/Function |
645
+ | --------------------- | ------------------------------------------- |
646
+ | List documents | `useDocuments`, `usePaginatedDocuments` |
647
+ | Read document | `useDocument`, `useDocumentProjection` |
648
+ | Edit field | `useEditDocument` |
649
+ | Publish/Delete/Create | `useApplyDocumentActions` + action creators |
650
+ | GROQ query | `useQuery` |
651
+ | Check permissions | `useDocumentPermissions` |
652
+ | Listen to changes | `useDocumentEvent` |
653
+
654
+ ---
9
655
 
10
656
  ## Documentation
11
657
 
12
- - Familiarize yourself with the App SDK via a conceptual overview, a quickstart guide, and a step by step walkthrough on **[the Sanity Docs site](https://sanity.io/docs/app-sdk)**
13
- - Go in depth with the **[App SDK reference docs](https://reference.sanity.io/_sanity/sdk-react)**
14
- - View example implementations on the **[SDK Explorer](https://sdk-explorer.sanity.io)**
658
+ - **[Sanity Docs](https://sanity.io/docs/app-sdk)** — Conceptual overview, quickstart guide, and step-by-step walkthrough
659
+ - **[App SDK Reference](https://reference.sanity.io/_sanity/sdk-react)** — In-depth API documentation
660
+ - **[SDK Explorer](https://sdk-explorer.sanity.io)** — Example implementations
661
+ - **[Migration Guide](./guides/0-Migration-Guide.md)** — Upgrading from previous versions
662
+ - **[Learn Course](https://www.sanity.io/learn/course/build-content-apps-with-sanity-app-sdk)** — Interactive video tutorial
15
663
 
16
664
  ## License
17
665