Discussion on fetching fresh data from API in custom document view component using useClient() and a reference implementation of useListeningQuery hook.

5 replies
Last updated: Apr 25, 2023
Hi Sanity Community — I’m trying to fetch fresh data from the API in a custom document view component with
useClient()
. I want the fetched data to match the timestamp of
document.displayed._updatedAt
, which the custom view component is supplied with from Structure Builder. If I delay the fetch, the data matches (i.e. the content lake has time to update), but I’d much rather avoid the arbitrary delay. I’ve tried adding the pause after an initial non-matching fetch and calling the function recursively, but even with a delay here, the values never match — in fact, they never even change (see image below the code snippet). Any ideas on why this isn’t working, or how I might otherwise eliminate the hard-coded delay? Thank you!

  const fetchConcepts = async (): Promise<any> => {
    try {
      // the current working version puts a delay here, which returns consistent results, but is not ideal:
      // await new Promise((resolve) => setTimeout(resolve, 1000))
      const res = await client.fetch(trunkBuilder(), {id: dId})
      // what I would like to do is check the _updatedAt value of the displayed document and the response, and if they don't match, wait a bit and try again:
      if (res._updatedAt !== document.displayed._updatedAt) {
        console.log('res._updatedAt', res._updatedAt)
        console.log('document.displayed._updatedAt', document.displayed._updatedAt)
        await new Promise((resolve) => setTimeout(resolve, 250))
        // unfortunately, this doesn't work. The fetched document is not updated, and the query returns the same results. Forever:
        return fetchConcepts()
      } else if (res.topConcepts === null && res.orphans.length < 1) {
        setNoConcept(true)
      } else {
        setNoConcept(false)
        setConcepts(res)
      }
    } catch (error) {
      // eslint-disable-next-line no-console
      console.log('Error: ', error)
      setIsError(true)
    }
    return setIsLoading(false)
  }
AI Update

The issue you're experiencing is related to how useClient() is configured in your Studio component. By default, the client may be using the CDN or a specific perspective that doesn't immediately reflect the latest changes.

Based on the official Studio React Hooks documentation, here's how to properly configure useClient() to fetch fresh data:

const client = useClient({ apiVersion: '2023-01-01' }).withConfig({ perspective: 'raw' })

Why this matters:

  • Perspective: Setting perspective: 'raw' ensures you're querying both published and draft documents, which is typically what you want in Studio when viewing the latest changes
  • CDN caching: When you use useClient() without additional configuration, it may be hitting cached data. The .withConfig() method allows you to override settings

Better approach using useDocumentStore():

Instead of polling with delays, you should use Studio's built-in document listening capabilities. The useDocumentStore() hook provides real-time updates:

import { useMemo } from 'react'
import { useDocumentStore } from 'sanity'
import { useObservable } from 'react-rx'

const INITIAL_STATE = []

const fetchConcepts = () => {
  const docId = document.displayed._id
  const documentStore = useDocumentStore()
  
  const observable = useMemo(() => 
    documentStore.listenQuery(
      trunkBuilder(),
      { id: docId },
      {}
    )
  , [documentStore, docId])
  
  const results = useObservable(observable, INITIAL_STATE)
  
  // results will automatically update when the document changes
  useEffect(() => {
    if (results?._updatedAt === document.displayed._updatedAt) {
      if (results.topConcepts === null && results.orphans.length < 1) {
        setNoConcept(true)
      } else {
        setNoConcept(false)
        setConcepts(results)
      }
      setIsLoading(false)
    }
  }, [results])
}

Why your recursive approach wasn't working:

Even with delays, you were likely hitting cached responses. The CDN doesn't invalidate on every request—it serves cached content until the cache updates. Your recursive calls were just fetching the same cached data repeatedly, which is why the _updatedAt values never changed.

The listenQuery() method from useDocumentStore() eliminates this problem entirely by establishing a real-time listener that's aware of document mutations as they happen in Studio. This is the recommended pattern for custom document views that need to stay in sync with document changes.

Have you tried querying from documentStore instead of the client? It could be ever so slightly up to date than the client.
I’ve got a reference implementation of it here in this useListeningQuery hook which you might find useful:

https://github.com/SimeonGriggs/sanity-plugin-utils#uselisteningquery
I haven’t tried that yet — but I will. That looks like it could replace my whole
useEffect()
hook — and maybe even all the fussy logic I’m using to keep
documentId
and
document.displayed._id
in sync. Thanks for the suggestion and the link!
I’ve built a lot of little plugins that needed querying with live updates and loading/error states – so I’ve used this on a number of projects. LMK if you have any feedback though!
Fantastic — thanks, I’ll do that!
So … that hook cut my fetch code down from 107 lines to 30 🤩There are a couple update quirks to work out, but I think those are in my components (which sorely need to be streamlined). I’ve also got some near term uses for a couple of these other hooks — thank you for putting these together!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?