How to execute a dynamic query in deskStructure every time in Sanity.io.

2 replies
Last updated: Oct 27, 2023
In the deskStructure, I have a `S.documentList('page').title('All Pages').filter(
_type == 'page' && ${dynamicQuery()}
)` The
dynamicQuery()
call could change the query each time.
Is there any way to get the deskStructure to execute it every time, rather than caching ahead of time and running the same query? I can see that having in
child()
a
context.documentStore.listenQuery
executes every time, it'd be great if I could pipe that into the documentList. I'm trying to avoid a hacky solution that uses listenQuery and just ignores the response so that
S.documentList
gets executed every time.
AI Update

The Structure Builder in Sanity Studio does cache and memoize structure definitions by design, which is causing your issue. However, there's a proper way to achieve dynamic filtering that re-evaluates on each access.

The key is understanding that child() resolvers are called every time a pane is opened, while top-level structure definitions are evaluated once. Based on the Structure Builder documentation, you can leverage this behavior:

S.listItem()
  .title('Filtered Pages')
  .child(() => 
    S.documentList()
      .title('All Pages')
      .filter(`_type == 'page' && ${dynamicQuery()}`)
  )

By wrapping your S.documentList() inside a child() function, the entire document list (including its filter) gets re-evaluated each time the pane is opened, rather than being cached at structure initialization time.

If you need this at the root level (not nested under a list item), you can make your entire structure resolver return items with child functions:

export const structure: StructureResolver = (S) =>
  S.list()
    .id('root')
    .title('Content')
    .items([
      S.listItem()
        .title('Pages')
        .child(() => 
          S.documentList()
            .title('All Pages')
            .filter(`_type == 'page' && ${dynamicQuery()}`)
        ),
      // other items...
    ])

The Structure Builder supports asynchronous operations, so if your dynamicQuery() needs to fetch data, you can also make the child resolver async:

.child(async () => {
  const dynamicPart = await someAsyncOperation()
  return S.documentList()
    .title('All Pages')
    .filter(`_type == 'page' && ${dynamicPart}`)
})

This approach is cleaner than using context.documentStore.listenQuery and ignoring the response, as it works with the Structure Builder's intended architecture where child resolvers are naturally re-evaluated on navigation. The child() function is specifically designed to be called every time you navigate to that pane, making it perfect for dynamic queries that need fresh evaluation.

I've managed to find a way through creating an Observable, but it'd be great to know if there's a better way:

.child(new Observable((subscriber) => {
             subscriber.next(dynamicQuery())
            }).pipe(map((groqQuery) => {
                return S.documentList('page')
                  .title('All Pages')
                  .menuItems(S.documentTypeList('page').getMenuItems())
                  .filter(`_type == "page" && ${groqQuery}`)
            })))

I've actually created an elegant solution from this. abstracting the Observable away from here, creating a wrapper `getDynamicQuery((query) => S.documentList('page').filter(
_type == 'page' ${query}
)` .
In my case, the dynamic query is based off of localStorage. So in
getDynamicQuery
I create an
Observable
and the
subscriber
to an array. Then, there's a global
window.addEventListener('storage', event => { ... })
that loops through the callback array and calls
subscriber.next(...)
with the query based off of the updated storage value.
All open tabs then update when the value is changed, so there are no inconsistencies or confusing behaviour.

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?