CoursesBuild content apps with Sanity App SDKuseDocuments

Build content apps with Sanity App SDK

Lesson
4

useDocuments

Log in to watch a video walkthrough of this lesson
Log in
Video thumbnail
Performant querying for a live-updating list of documents has never been simpler.
Log in to mark your progress for each Lesson and Task

Maybe the most basic thing we can do when creating an application is query and render a list of documents. In this lesson, you'll do it using the useDocuments hook.

Before we do, let's interrogate this decision.

Sanity Client is the primary way JavaScript applications are built to interact with Sanity's APIs. The App SDK is a wrapper of Sanity Client for building apps and solves much of the UI complexity that comes with working with a lower-level client.

For example, when you query documents with client.fetch, the list of documents will not update in real-time as documents change. You might also unknowingly fetch 10,000 documents. Edits made to these documents will not automatically create per-render optimistic updates in the UI.

All complexity and performance concerns are bundled up in the App SDK's React hooks, making it simpler and faster for us to build better content applications.

The App SDK provides the useQuery hook, which takes a GROQ query. We could use this hook for all our data fetching. However, many other hooks in the App SDK for React require "document handles" to be passed in as parameters.

These can be created ad hoc from values in a document. Still, it's simpler to retrieve document handles in a parent component and pass them down to child components, which perform fetches or actions using those handles.

There are some patterns where it makes more sense to use useQuery instead of useDocuments, such as when a parent component needs to know specific values of each document. For example, a component may need to know which day an event is on to render it into a calendar or the geolocation of an event to plot it on a map.

In these instances, you may be better off fetching with useQuery documents and their values in the parent component. Just be aware that this can lead to overfetching.

A document handle contains at least two and up to four useful pieces of information about a document to identify its type and origin. By keeping the data returned by fetches of documents smaller, we maintain focus on improved performance in our applications.

{
"dataset": "production",
"documentId": "116d2c7a-d1de-4d00-9a88-8ac65ceaad10",
"documentType": "feedback",
"projectId": "xe385msc"
}

In Server-Side Rendered (SSR) web application frontends, you have likely formed a habit of fetching everything your web page needed in one huge GROQ query using client.fetch.

This isn't necessary in a Single Page Application (SPA).

The happy path for App SDK apps is to filter for specific documents using useDocuments and pass down the returned document handles to components which individually fetch, edit and take actions on documents.

Any concerns about caching, real-time and optimistic updates are all taken care of by the App SDK.

Create a new component called Feedback, which will be the parent component of all our UI.
import { Suspense, useState } from "react"
import { DocumentHandle } from "@sanity/sdk-react"
import { Card, Flex, Grid, Spinner } from "@sanity/ui"
import { styled } from "styled-components"
import { FeedbackList } from "./FeedbackList"
const ScreenHeightCard = styled(Card)`
height: 100vh;
overflow: scroll;
`
export function Feedback() {
const [selectedFeedback, setSelectedFeedback] =
useState<DocumentHandle | null>(null)
return (
<Grid columns={5}>
<ScreenHeightCard columnStart={1} columnEnd={3}>
<Suspense fallback={<Loading />}>
<FeedbackList
setSelectedFeedback={setSelectedFeedback}
selectedFeedback={selectedFeedback}
/>
</Suspense>
</ScreenHeightCard>
<ScreenHeightCard borderLeft columnStart={3} columnEnd={6}>
{/* TODO: Add <FeedbackEdit /> form */}
</ScreenHeightCard>
</Grid>
)
}
function Loading() {
return (
<Flex justify="center" align="center" width="fill" height="fill">
<Spinner />
</Flex>
)
}
Don't forget your Suspense boundaries. The App SDK React Hooks use Suspense for data fetching. This means any component which uses one of these hooks could cause a re-render further up the component tree. Since this Feedback component will be rendering both the FeedbackList and FeedbackEdit form components, without being wrapped in Suspense an update in one component would force both to re-render.
Create another component to query for the feedback documents.
import { type DocumentHandle, useDocuments } from "@sanity/sdk-react"
import { Stack, Button } from "@sanity/ui"
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) => (
<pre key={feedback.documentId}>{JSON.stringify(feedback, null, 2)}</pre>
))}
{hasMore && <Button onClick={loadMore} text="Load more" />}
</Stack>
)
}

Lastly, you'll need to load the Feedback component into the main App.

Update App.tsx to replace ExampleComponent with Feedback
import {Feedback} from "./Feedback"
export default function App() {
// ...sanityConfigs, Loading
return (
<SanityUI>
<SanityApp config={sanityConfigs} fallback={<Loading />}>
<Feedback />
</SanityApp>
</SanityUI>
)
}

In your application you should now see a list of document handles rendered into the UI.

So you now have an application that can query documents, but not any useful information about them. Let's fix that in the next lesson.

You have 3 uncompleted tasks in this lesson
0 of 3