Filtering a reference list with dynamic filter in Sanity.io

13 replies
Last updated: Jan 28, 2021
Hey all! Real quick question for you. I'm trying to filter a reference list with a dynamic filter. This is where I'm currently at but it doesn't seem to do anything right now:
filter: ({ document }) => {
  const ref = document.content?.product?._ref
  if (!ref) return
  return {
    filter: 'product._ref == $product',
    params: {
      product: ref,
    },
  }
},
The document that this is being run within has a field
content.product
which references a
product
, and the document type I am trying to filter here should also reference the same
product
. You can't use
console.log
within filter functions, so I have no way to be able to inspect what
document
contains. I've tried a number of different ways of going about this, including checking for the presence of
document.content
and
document.content.product
first, but nothing seems to make a difference here.
Any ideas, anyone?
AI Update

Looking at your code, the issue is likely that you're trying to access nested properties that may not exist yet when the filter function runs. Here are a few things to check:

The Problem:

The document object in filter functions represents the current document being edited, so document.content?.product?._ref should work, but you need to ensure the document structure matches and that you're filtering correctly.

Solution:

Based on the Sanity documentation on reference filters, here's a corrected version:

filter: ({ document }) => {
  const productRef = document?.content?.product?._ref
  
  // Make sure we have a product reference before applying filter
  if (!productRef) {
    return {} // Return empty object to show all results when no product selected
  }
  
  return {
    filter: 'product._ref == $productRef',
    params: {
      productRef: productRef,
    },
  }
}

Key points to understand:

  1. Always check for existence - Your optional chaining looks good, but make sure you're checking at each level
  2. Return an empty object {} when there's no filter to apply (not undefined or nothing)
  3. The filter applies to target documents - If you're filtering a reference field that points to some document type, the GROQ filter runs against those target documents, not your current document
  4. Parameter naming - I changed product to productRef in params to be clearer, but the actual name doesn't matter as long as it matches in both the filter string and params object

Debugging tips:

Since you can't use console.log inside the filter function, try these approaches:

  1. Check your schema structure - Make sure the field path content.product actually exists in your document
  2. Verify the target documents - The documents you're trying to reference should also have a product reference field that you're filtering against
  3. Test the GROQ filter separately - Try running your GROQ query in Vision or the GROQ playground to verify it works
  4. Use the document inspector - In Studio, use the "Inspect" option on documents to see their actual structure

If you're still having issues, it would help to know:

  • What document type has this filter?
  • What document types are you referencing (the to field)?
  • Do the target documents have a product reference field?
The filter setup seems correct. Could you share your schema definition for the relevant document as well as the
product
document type?
The filter setup seems correct. Could you share your schema definition for the relevant document as well as the
product
document type?
Schema for document:
import React from 'react'
import Tabs from 'sanity-plugin-tabs'

const IntroStyle = (props) => (
  <p style={{ textAlign: 'center', fontSize: '1.25rem', fontWeight: '600' }}>
    {props.children}
  </p>
)

export const ProductPage = {
  title: 'Product Page',
  name: 'productPage',
  type: 'document',
  fields: [
    {
      name: 'content',
      type: 'object',
      inputComponent: Tabs,

      fieldsets: [
        { name: 'main', title: 'Main', options: { sortOrder: 10 } },
        { name: 'meta', title: 'Meta', options: { sortOrder: 30 } },
        { name: 'config', title: 'Options', options: { sortOrder: 50 } },
      ],

      fields: [
        {
          title: 'Product',
          name: 'product',
          type: 'reference',
          to: [{ type: 'product' }],
          fieldset: 'main',
          description: 'Select the product that this product page is about',
          options: {
            filter: `count(*[_type == 'productPage' && references(^._id)]) == 0`,
          },
        },
        {
          title: 'Page title',
          name: 'title',
          type: 'string',
          fieldset: 'main',
          description:
            '[OPTIONAL] Customise the title of this product page, if you do not want to use the product name',
        },
        {
          title: 'Parent page',
          name: 'parentPage',
          type: 'reference',
          to: [{ type: 'page' }, { type: 'productPage' }],
          description:
            '[optional] set a parent page for this page to appear below in url structure',
          fieldset: 'main',
        },
        {
          title: 'Page content',
          name: 'pageContent',
          type: 'array',
          of: [
            {
              type: 'block',
              styles: [
                { title: 'Normal', value: 'normal' },
                {
                  title: 'Intro',
                  value: 'intro',
                  blockEditor: {
                    render: IntroStyle,
                  },
                },
                { title: 'H2', value: 'h2' },
                { title: 'H3', value: 'h3' },
                { title: 'H4', value: 'h4' },
                { title: 'Quote', value: 'quote' },
              ],
            },
            { type: 'productPage.hero' },
            { type: 'productPage.intro' },
            { type: 'columns' },
            {
              type: 'image',
              fields: [
                {
                  title: 'Alt text',
                  name: 'alt',
                  type: 'text',
                  options: {
                    isHighlighted: true,
                  },
                },
              ],
              options: {
                metadata: ['palette'],
                hotspot: true,
              },
            },
            { type: 'video' },
            { type: 'gallery' },
            { type: 'page.imageSlider' },
            { type: 'page.richTextWithMedia' },
          ],
          fieldset: 'main',
        },
        {
          title: 'SEO / Meta Settings',
          name: 'meta',
          type: 'page.meta',
          fieldset: 'meta',
        },
        {
          title: 'Enable sticky "Get a Quote" CTA?',
          name: 'hasStickyQuoteButton',
          type: 'boolean',
          description:
            '[optional] Enable the sticky header (desktop) / footer (mobile) Get a Quote CTA on this page',
          fieldset: 'config',
        },
        {
          title: 'Technical files',
          name: 'technicalFiles',
          description:
            '[optional] Select which technical files associated with this product to show on the product page',
          type: 'array',
          of: [
            {
              title: 'File',
              type: 'reference',
              to: [
                {
                  type: 'technicalFile',
                  options: {
                    filter: ({ document }) => {
                      const ref = document.content?.product?._ref
                      if (!ref) return {}
                      return {
                        filter: 'product._ref == $product',
                        params: {
                          product: ref,
                        },
                      }
                    },
                  },
                },
              ],
            },
          ],
          fieldset: 'config',
        },
      ],
    },
  ],
  preview: {
    select: {
      pageTitle: 'content.title',
      productTitle: 'content.product.title',
    },
    prepare(selection) {
      const { pageTitle, productTitle } = selection
      return {
        title: pageTitle || productTitle,
      }
    },
  },
}
Technical file schema:
export const TechnicalFile = {
  title: 'File',
  name: 'technicalFile',
  type: 'document',
  fields: [
    {
      title: 'Document Title',
      name: 'title',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
    {
      title: 'File',
      name: 'file',
      type: 'file',
      validation: (Rule) => Rule.required(),
    },
    {
      title: 'Document Code',
      name: 'code',
      type: 'string',
    },
    {
      title: 'Product',
      name: 'product',
      type: 'reference',
      to: [{ type: 'product' }],
    },
    {
      title: 'Document Type',
      name: 'doctype',
      type: 'string',
      options: {
        list: [
          'Installation instructions',
          'Technical drawings',
          'CAD file',
          'Product data sheet',
        ],
      },
      validation: (Rule) => Rule.required(),
    },
    {
      title: 'Description',
      name: 'description',
      type: 'text',
    },
    {
      title: 'Image',
      name: 'image',
      description:
        '[optional] Add an image to override the default product image to be shown in the file listing',
      type: 'image',
      fields: [
        {
          title: 'Alt text',
          name: 'alt',
          type: 'text',
          options: {
            isHighlighted: true,
          },
        },
      ],
      options: {
        metadata: ['palette'],
        hotspot: true,
      },
    },
  ],
}
Product is pretty much placeholder right now:
export const Product = {
  title: 'Product',
  name: 'product',
  type: 'document',
  fields: [
    {
      title: 'Product name',
      name: 'title',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
    {
      title: 'Featured image',
      name: 'image',
      type: 'image',
      fields: [
        {
          title: 'Alt text',
          name: 'alt',
          type: 'text',
          options: {
            isHighlighted: true,
          },
        },
      ],
      options: {
        metadata: ['palette'],
        hotspot: true,
      },
      validation: (Rule) => Rule.required(),
    },
  ],
}
To be clear, the document selector currently shows all documents of type
technicalFile
, rather than filtering them.
Could you try moving your
options
out of the
to[]
array? So one level higher?
As follows:
{
  title: 'File',
  type: 'reference',
  to: { type: 'technicalFile' },
  options: {
    filter: ({ document }) => {
      const ref = document.content?.product?._ref
      if (!ref) return {}
      return {
        filter: 'product._ref == $product',
        params: {
          product: ref,
        },
      }
    },
  },
},
Yes, that's it. I'm an idiot and didn't see that I had it in the wrong place entirely! Thanks so much
user M
!
An error would have helped I'm sure - not sure why it worked before 😄
It didn't. That was poor wording on my part. Seeing as Sanity is fully TypeScripted up now, this feels like something that could potentially be fairly easy to catch, as it is a property in a place that it shouldn't be
Yep, agreed. We could definitely improve error messages here and save some hair-pulling 😉

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?