CoursesBuild content apps with Sanity App SDKuseDocumentProjection

Build content apps with Sanity App SDK

Lesson
5

useDocumentProjection

Log in to watch a video walkthrough of this lesson
Log in
Video thumbnail
Pick just the content you need from individual documents, and only when a component is rendered in view.
Log in to mark your progress for each Lesson and Task

We have a list of document handles, but we need more information about each document. Let's create a component that uses these handles to fetch more values from each document.

Create a new component to visualize the value of the status field in a document throughout our application.
import { Badge } from "@sanity/ui"
type StatusBadgeProps = {
status?: string
fontSize?: number
}
export function StatusBadge({
status = "PENDING",
fontSize = 2,
}: StatusBadgeProps) {
return (
<Badge
tone={
status === "approved"
? "positive"
: status === "spam"
? "caution"
: "default"
}
padding={2}
fontSize={fontSize}
>
{status.toUpperCase()}
</Badge>
)
}
Create a component to retrieve and display values from a document by its handle
import { useRef } from "react"
import { DocumentHandle, useDocumentProjection } from "@sanity/sdk-react"
import { Box, Stack, Text } from "@sanity/ui"
import { StatusBadge } from "./StatusBadge"
type FeedbackPreviewData = {
_createdAt: string
content: string | null
author: string | null
email: string | null
status: string
}
export function FeedbackPreview(props: DocumentHandle) {
const previewRef = useRef<HTMLDivElement>(null)
const { data, isPending } = useDocumentProjection<FeedbackPreviewData>({
...props,
ref: previewRef,
projection: `{
_createdAt,
content,
author,
email,
"status": coalesce(status, "PENDING")
}`,
})
const showPlaceholder = isPending && !data
return (
<Stack ref={previewRef} space={3}>
<Text size={2} weight="semibold" textOverflow="ellipsis">
{showPlaceholder ? "..." : data.author}
</Text>
<Text muted size={1} textOverflow="ellipsis">
{showPlaceholder
? "..."
: data.email + " " + data._createdAt.split("T")[0]}
</Text>
<Text size={2} textOverflow="ellipsis">
{showPlaceholder ? "..." : data.content}
</Text>
<Box>
<StatusBadge status={data.status} fontSize={1} />
</Box>
</Stack>
)
}

There's a few key things to look at in this component.

  • useDocumentProjection receives the passed-in document handle as props, and then declares a GROQ "projection" which retrieves values from the document.
  • The ref being passed into the hook is attached to the outermost Stack component. This will ensure that the content returned by this projection is only queried when the component is rendered and visible on the page. Another small but important performance win!
Throughout this course we're manually creating Types. This is because TypeGen support for App SDK currently uses experimental packages and may change in future. See the documentation for the most current implementation method.
See App SDK and TypeGen in the documentation

Now you have a component to fetch individual documents, let's update the feedback list component to use it.

Update the FeedbackList component to render the FeedbackPreview component
import { Suspense } from "react"
import { type DocumentHandle, useDocuments } from "@sanity/sdk-react"
import { Stack, Button, Spinner } from "@sanity/ui"
import { FeedbackPreview } from "./FeedbackPreview"
type FeedbackListProps = {
selectedFeedback: DocumentHandle | null
setSelectedFeedback: (feedback: DocumentHandle | null) => void
}
export function FeedbackList({
selectedFeedback,
setSelectedFeedback,
}: FeedbackListProps) {
const { data, hasMore, loadMore } = useDocuments({
documentType: "feedback",
})
return (
<Stack space={2} padding={5}>
{data?.map((feedback) => {
const isSelected = selectedFeedback?.documentId === feedback.documentId
return (
<Button
key={feedback.documentId}
onClick={() => setSelectedFeedback(feedback)}
mode={isSelected ? "ghost" : "bleed"}
tone={isSelected ? "primary" : undefined}
>
<Suspense fallback={<Spinner />}>
<FeedbackPreview {...feedback} />
</Suspense>
</Button>
)
})}
{hasMore && <Button onClick={loadMore} text="Load more" />}
</Stack>
)
}

You should now have the feedback items rendered as a list of buttons. Most importantly you'll see values from each document in each button. And if any other author makes changes to these documents, you'll see those values update live!

You can click to select them, they just won't do anything yet. In the next lesson you can start to building a form to edit each feedback document.

You have 3 uncompleted tasks in this lesson
0 of 3