Dynamic GROQ Query in JavaScript

    As we use Sanity.io's GROQ more and more, we'll soon get yourselves into situations where we will have to construct our queries dynamically. A very good example of that is say, we're working on getting all products based on different attributes.

    Imagine we have the following product filters below and their options:

    color = ["Blue", "Red", "Orange", "Green"]
    size = ["Small", "Medium", "Large"]
    gender = ["Male", "Female"]

    Now, the user first then filters the products by selecting a color attribute of value Red.

    We could then write our query like this below:

    *[color == "Red"]

    But let's say, now our user also selects a size of value Small, and so now, it looks like this:

    *[color == "Red" && size == "Small"]

    And once more, the user selects a gender of value Female, so finally looking like this. Whoops...

    *[color == "Red" && size == "Small" && gender == "Female"]

    In situations like this, we'll need to write our GROQ query dynamically, that's because only then we can formulate it when the user has done a selection. What's more, is that the values Red, Small, and Female are dynamic too. So basically, we have two things to do:

    1. Construct our query properly based on selected product filters
    2. Substitute the values in

    In JavaScript, we could use template literals to take care of the second task like this below:

    const { color, size, gender } = selection // { color: "Red", size: "Small", gender: "Female" }
    const query = `*[color == "${color}" && size == "${size}" && gender == "${gender}"]`
    
    console.log(query) // *[color == "Red" && size == "Small" && gender == "Female"]

    But if we use the code above when the user has filtered the products by color only then we'll get an incorrect GROQ query, sometimes malformed:

    *[color == "Red" && size == "" && gender == ""]

    Not good! 😔

    One last thing before we go all out! The above example is pretty contrived, GROQ queries aren't just as simple as that. It can get more complicated especially when we're working with all different shapes of data. In some challenging datasets, there are times that we'll do one or many forms of projections, joins, filters, orders, pipes, and so many more.

    There are tons of possibilities really and as another example, here below we added some projections to pick specific fields we want to get:

    *[color == "Red" && size == "Small" && gender == "Female"] {
      name,
      description,
      price,
      categoryType->{name}
    }

    We'll get back to that later. But for now, the real question is:

    How can we create a dynamic GROQ query that will be able to handle any complexity and flexible enough?

    If you haven't had the chance to learn GROQ, you might be interested in learning the basics here and/or probably check out why I could think GROQ as a GraphQL alternative here.

    Basic Dynamic GROQ Query

    For a start, we can use template literals to do some string interpolation.

    const { color, size, gender } = selection // { color: "Red", size: "Small", gender: "Female" }
    const query = `*[color == "${color}" && size == "${size}" && gender == "${gender}"]`

    This will give us exactly what we're looking for and the value of our query will be:

    *[color == "Red" && size == "Small" && gender == "Female"]

    To take this a step further, instead of writing the filters by ourselves, we can use the Array.reduce function on our object to construct it dynamically.

    const { color, size, gender } = selection // { color: "Red", size: "Small", gender: "Female" }
    
    const filters = Object.entries(selection)
      .reduce((result, entry) => {
        const [key, value] = entry
        return [...result, `${key} == "${value}"`]
      }, [])
      .join(' && ')
    
    const query = `*[ ${filters} ]`

    Awesome. Now we're unto something…

    But can we take this a step further? Remember, GROQ queries can get more complicated.

    Back from our previous example:

    *[color == "Red" && size == "Small" && gender == "Female"] {
      name,
      description,
      price,
      categoryType->{name}
    }

    If we digest this out, we'll most probably come with this structure:

    const query = `*[ ${filters} ] {
      ${projections}
    }`

    And so, we could create projections too to take care of that. Nice!

    const projections = [
      'name',
      'description',
      'price',
      'categoryType->{name}',
    ].join(', ')

    Now, what if we the user has decided to order the results by price? And our query looks like this now below.

    *[color == "Red" && size == "Small" && gender == "Female"] {
      name,
      description,
      price,
      categoryType->{name}
    } | order(price.amount desc) // highlight-line

    Well, we can create order you'd say, just like below:

    const order = `order(${sortKey}, ${sortDirection})`

    And so, altogether, it'll be like this:

    const { color, size, gender } = selection // { color: "Red", size: "Small", gender: "Female" }
    const { sortKey, sortDirection } = sorting // { sortKey: "price.amount", sortDirection: "desc" }
    
    const filters = Object.entries(selection)
      .reduce((result, entry) => {
        const [key, value] = entry
        return [...result, `${key} == "${value}"`]
      }, [])
      .join(' && ')
    
    const projections = [
      'name',
      'description',
      'price',
      'categoryType->{name}',
    ].join(', ')
    
    const order = sorting ? `| order(${sortKey}, ${sortDirection})` : ''
    
    const query = `*[ ${filters} ] {
      ${projections}
    } ${$order}`

    Sounds like we've made some progress but we can do more. Yes! See below.

    Using Tagged Templates for Dynamic GROQ Query

    A more advanced form of template literals is tagged templates. This allows us to better compose our query and supply the values later on instead of providing them ahead.

    const selection // { color: "Red", size: "Small", gender: "Female" }
    const sorting // { sortKey: "price.amount", sortDirection: "desc" }
    
    // tagged template function
    function groq(strings, ...keys) {
      return function (...values) {
        let dict = values[values.length - 1] || {}
        let result = [strings[0]]
        keys.forEach(function (key, i) {
          let value = Number.isInteger(key) ? values[key] : dict[key]
          result.push(value, strings[i + 1])
        })
        return result.join("")
      }
    }
    
    // Let's build our function to make the query for us
    const makeQuery = groq`*[ ${"filters"} ] {
      ${"projections"}
    } ${"order"}`
    
    // You can use this anywhere and provide values however you want to
    const query = makeQuery({
      filters: Object.entries(selection)
        .reduce((result, entry) => {
          const [key, value] = entry
          return [...result, `${key} == "${value}"`]
        }, [])
        .join(" && "),
      projections: ["name", "description", "price", "categoryType->{name}"].join(
        ", "
      ),
      order: (() => {
        if (!sorting) return ""
        const { sortKey, sortDirection } = sorting
        return `| ${sortKey} ${sortDirection}`
      })(),
    })

    For the sake of example, I created a groq tagged template function above but I think you're probably better of using minify-groq created by Espen Hovlandsdal of Sanity.io's Principal Engineer. Check it out!

    What's Next?

    As I explored this area, there are packages you can use to simplify this task for you. Just take note though that it may not cover all use cases you might have. I'll list them out in the Resources below. I personally just make use template literals or tagged templates and/or a combination of both.

    So here you go on how to build a dynamic GROQ query in javascript and some examples.

    What do you think? I'd like to hear your opinion.

    Resources

    • GROQ tagged template literal - Used to signal that it represents a GRQO-query. The result will be the exact same string as the input  -  this is currently helpful for getting syntax highlighting in editors, but in the future, it might also parse and validate queries, strip unnecessary whitespace, and similar.
    • minify-groq - Minifies a GROQ-query by reducing unnecessary whitespace and supports placeholder(s) just like in my example above. I would personally say that you use this.
    • How Queries Work - GROQ - A tutorial on using the Sanity query language GROQ. This will give you an idea of how GROQ works.
    • sanity-typed-queries - More than just a query builder but also a schema generator that is fully-typed and works in JavaScript and TypeScript.
    • sanity-query-helper - provides an API that might be easier to understand to build queries.

    More articles

    Here's How To Hide Admin Panel Menus In Strapi

    How to hide admin panel menus in Strapi. This is a quick guide to help you hide the admin panel menus in Strapi.

    Read more

    End-to-End Testing with Sanity Studio & Playwright

    Learn how to do end-to-end test Sanity Studio with Playwright

    Read more

    Let's talk about your project