Querying reverse relationships in Sanity with GROQ

3 replies
Last updated: Aug 29, 2021
has anyone got an example of a reverse relationship? For example, I have "work" and "work categories" as 2 separate documents. On "work" i reference "work category" - I want to loop over the work categories and show the related work items from each, is there a way I can do this with Sanity? (I'm using NextJs for the FE)
AI Update

Yes! This is a perfect use case for GROQ's references() function, which lets you do reverse reference lookups. Since you have "work" documents that reference "work categories", you can query all categories and then find which works reference each one.

Here's how to do it with a GROQ query:

*[_type == "workCategory"] {
  _id,
  title,
  "relatedWorks": *[_type == "work" && references(^._id)] {
    _id,
    title,
    slug,
    // any other fields you need from work
  }
}

The key part is references(^._id) - this checks if any "work" documents contain a reference to the current category's ID. The ^ operator accesses the parent scope (the category being iterated over).

In your Next.js frontend, you'd use this with the Sanity client:

import { client } from '@/sanity/lib/client'

const query = `*[_type == "workCategory"] {
  _id,
  title,
  "relatedWorks": *[_type == "work" && references(^._id)] {
    _id,
    title,
    slug
  }
}`

export async function getWorkCategories() {
  const categories = await client.fetch(query)
  return categories
}

Then you can loop over the categories in your component:

{categories.map((category) => (
  <div key={category._id}>
    <h2>{category.title}</h2>
    <ul>
      {category.relatedWorks.map((work) => (
        <li key={work._id}>{work.title}</li>
      ))}
    </ul>
  </div>
))}

The references() function documentation explains this pattern in detail, and there's also a helpful backlinks recipe that shows similar reverse reference patterns.

One performance note: if you have many works per category, you might want to add pagination or limits to the subquery to keep response sizes manageable.

Sure! I don’t have your exact schemas, but to get a gist of it:• Assuming that your
work
document has an array of references to
workCategory
called
workCategories
• Here using a sub-query where I match the parent
_id
, that is, the
_id
of the
workCategory
document, to an
_ref
inside of the references field in the
work
document
*[_type == "workCategory"]{
  _id,
  name,
  // using the parent operator ^ 
  "relatedWork": *[^._id in workCategories[]._ref]{
    _id,
    name,
    // other fields
  }
}
This is a slightly different query than what you requested, where ‘related’ is an array of unique work that has at least 1 of the parent work document’s categories

*[_id == "work-2"][0] {
  ...,
  "related": *[
    _type == "work" &&
    _id != "work-2" &&
    count(cats[@._ref in ^.^.cats[]._ref]) > 0
  ] {
    _id,
  }
}

https://groq.dev/7XeyO5v20NxEhj2aUjHBY4
I also add an example with User’s query:


*[_id == "work-2"][0] {
  ...,
  "related": *[_type == "cat" && _id in ^.cats[]._ref] {
    _id,
    "workDocs": *[_type == "work" && _id != "work-2" && references(^._id)] {
    _id}
  }
}

https://groq.dev/7XeyO5v20NxEhj2aUjHZBY
This is a slightly different query than what you requested, where ‘related’ is an array of unique work that has at least 1 of the parent work document’s category

*[_id == "work-2"][0] {
  ...,
  "related": *[
    _type == "work" &&
    _id != "work-2" &&
    count(cats[@._ref in ^.^.cats[]._ref]) > 0
  ] {
    _id,
  }
}

https://groq.dev/7XeyO5v20NxEhj2aUjHBY4
I also add an example with User’s query:


*[_id == "work-2"][0] {
  ...,
  "related": *[_type == "cat" && _id in ^.cats[]._ref] {
    _id,
    "workDocs": *[_type == "work" && _id != "work-2" && references(^._id)] {
    _id}
  }
}

https://groq.dev/7XeyO5v20NxEhj2aUjHZBY

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?