Issues with implementing custom features in Sanity v3, including accessing the sanity client within the prepare function and resolving errors with the getCliClient method...

29 replies
Last updated: May 31, 2024
hey sanity team,
I'm trying to customise some features in v3, specifically with the prepare function on document previews.
With the new approach of removing the part components and implementing hooks to access the sanity client, I am having issues implementing it inside the prepare function, since it is not a component itself. Normally I would find it useful to use the sanity client from within to be able to query the entire project to load data (like to follow a reference in the opposite direction), but now this seems impossible. Something like

preview: {
  select: {
    title: 'title',
    id: '_id'
  },
  prepare: async ({title, id}) => {
    // old approach, some pre-defined client from the parts system
    const {title: parentTitle } = await client.fetch('some query');

    // new approach with useClient hook won't work
    return {
      title: parentTitle + "-" + title,
    }
  }
}
Any suggestions?
AI Update

I understand your frustration! Unfortunately, the prepare function has a significant limitation in Sanity v3: it cannot make custom GROQ queries or access the Sanity client. This is explicitly documented as a limitation of the preview system.

The prepare function only receives the selection object you define in select, and there's no second context parameter with client access. This is by design - the select syntax handles dereferencing, but only in specific, predefined ways.

Your Options

Since you need to query data (like following references in the opposite direction), here are the viable approaches:

Instead of relying on prepare, you can create a custom preview component using the Form Components API. This is a React component where you can use the useClient hook:

import {useClient} from 'sanity'
import {defineType} from 'sanity'
import React from 'react'

const CustomPreview = (props: any) => {
  const client = useClient({apiVersion: '2024-01-01'})
  const [parentTitle, setParentTitle] = React.useState('')

  React.useEffect(() => {
    client.fetch('*[references($id)][0]{title}', {id: props.id})
      .then(result => setParentTitle(result?.title || ''))
      .catch(() => setParentTitle(''))
  }, [props.id, client])

  return props.renderDefault({
    ...props,
    title: parentTitle ? `${parentTitle} - ${props.title}` : props.title
  })
}

export default defineType({
  name: 'myDocument',
  type: 'document',
  components: {
    preview: CustomPreview
  },
  preview: {
    select: {
      title: 'title',
      id: '_id'
    }
  }
})

This approach gives you full React component capabilities, including hooks like useClient, and you can use the renderDefault function to wrap the existing preview behavior while enhancing it with your custom data.

2. Use select with Direct References

If you're following a forward reference (not reverse), you can use dot notation in select to access referenced document fields:

preview: {
  select: {
    title: 'title',
    parentTitle: 'parent.title'  // Works for forward references
  },
  prepare({title, parentTitle}) {
    return {
      title: parentTitle ? `${parentTitle} - ${title}` : title
    }
  }
}

But this only works for references defined on the document, not reverse lookups like your use case requires.

3. Denormalize Your Data

If the relationship is important for previews, consider storing the data you need directly on the document rather than requiring a reverse reference lookup. This isn't always practical, but it's the most performant option since it avoids additional queries.

Why This Changed

In v3, Sanity moved away from the parts system to a more explicit architecture. The prepare function is intentionally kept as a pure transformation function without side effects or async operations. For scenarios requiring data fetching, custom preview components with hooks are the recommended pattern - they're more powerful and align with modern React practices.

The trade-off is that custom components require more code, but they give you full control over the rendering and data fetching logic. The Form Components API provides this flexibility while maintaining type safety and integration with Studio's real-time collaboration features.

Hey
user R
! This method should work for you! We're working on getting it added to the migration cheat sheet.
oh that's quite simple....will definitely help! Thank you so much
You're welcome!
Hi
user M
— I think this thread solves a problem I’m also working through with V3 … but as soon as I add the
const client = getCliClient({ apiVersion: '2021-08-21' })
my studio crashes with an
Uncaught error: Buffer is not defined
message. I’m having a hard time decoding what might be going on. Any suggestions? Thanks!
I'm not certain! We're investigating!
Thank you!
I'm getting the same error using with this way
Yes, so am I.
What's the context you're using it in?
I’m attempting to use the client inside a schema file, as follows: https://sanity-io-land.slack.com/archives/C9Z7RC3V1/p1671502713201259
Here's my attempt in v3:
import { getCliClient } from "sanity/cli";

const client = getCliClient({ apiVersion: process.env.SANITY_STUDIO_VERSION });

export default defineType({
  name: "novel",
  title: "Novel",
  type: "document",
  fields: [
    defineField({
      name: "author",
      title: "Author",
      type: "reference",
      to: { type: "author" }
    })
  ],
  initialValue: async () => ({
    author: await client.fetch(`
      *[_type == "author"][0]{
        "_type": "reference",
        "_ref": _id
      }
    `)
  })
})
Got it! That's an incorrect usage of that method. If you're inside of the schema, you need to use the client inside of the context that's passed into most functions as the second argument:
initialValue: async (props, context) =>  {
          const {getClient} = context
          const client = getClient({ apiVersion: '2022-12-14'})
          //other code. 
        }
That's not very clearly documented in the cheat sheet, so we're working on updating the docs!
Ah, great! Thanks! Yes, it wasn't clear to me which functions receive
context
.
Exactly, we need to clear that up 😅
It works! No more error. Thanks again!
In my case is a more specific usage, it's inside the schema level but for use it with the document-internationalization plugin function that create a function for the slug validation: https://github.com/sanity-io/document-internationalization/issues/91
I think it's a problem with the function implementation, because they can use the context inside the function. Thank you Racheal I will try to open a PR
Oh I have 2 situations, one is using the
hidden
property into the field, here I get the
context
undefined:

export default {
  name: 'moduleRef',
  title: 'Module Reference',
  type: 'document',
  icon: GrFlows,
  i18n: true,
  initialValue: {
    __i18n_lang: 'en',
  },
  fields: [
    {
      name: 'moduleRef',
      type: 'reference',
      title: 'Module',
      to: [{ type: 'module' }],
      options: { filter: ` __i18n_lang == 'en'` },
    },
    {
      title: 'Module ID',
      name: 'moduleId',
      type: 'slug',
      description: 'This ID is necessary if some link need to scrolls to this specific module.',
    },
    {
      title: 'Variant',
      name: 'variant',
      type: 'string',
      components: {
        input: VariantField,
      },
      localize: false,
      hidden: async ({ parent }, context) => {
        console.log(context);
        const { getClient } = context;
        const client = getClient({ apiVersion: '2022-12-14' });
        const moduleData = await sanityClient.fetch(`
          *[_id == '${parent.moduleRef?._ref}'][0]
        `);
        console.log({ moduleData });
      },
    },
  ],
And another situation is using the
isUnique
property of a slug field getting *`Uncaught error: Buffer is not defined`*:
//Schema
export default {
  name: 'page',
  title: 'Pages',
  type: 'document',
  icon: GrDomain,
  i18n: true,
  initialValue: {
    __i18n_lang: 'en',
  },
  preview: {
    select: {
      title: 'internalName',
    },
  },
  fields: [
    {
      name: 'internalName',
      type: 'string',
      title: 'Internal name',
      description: 'For internal use within Sanity',
      localize: false,
    },
    {
      name: 'slug',
      type: 'slug',
      title: 'Slug',
      description: 'Used to generate the page URL (eg., "/careers" or "/contact")',
      validation: (rule: Rule): Rule => rule.required().error('You need a unique slug for the URL'),
      options: {
        source: 'internalName',
        maxLength: 200,
        isUnique: isSlugUnique,
      },
    },

//Function
export const isSlugUnique = (_, context: ConfigContext) => {
  const { getClient } = context;
  const client = getClient({ apiVersion: '2022-12-14' });
  return createIsSlugUnique(client);
};
Hidden doesn't support promises, so that's likely why it's not passing in the client. You'll need to use a custom component and conditional rendering to show/hide a field based off of a value inside of a reference. It should be receiving other arguments, though. Hmmm....I'll look into how it works with isUnique!
Ok, the client is located like so in your `isUnique`:
isUnique: async (value, {client}) => {}
Thank you Racheal, using a custom component as an input I don't know if I have control of the field label, for hidden it entirely. Do you know if we can do that? Maybe makes sense I create a new thread for that...
Related to the
isUnique
I'm getting the same error (https://github.com/sanity-io/document-internationalization/issues/91 ) using both (
client
and
getClient
) from the context
user U
you can use Field components to completely obscure the field, including the title. More on the difference between the two here .
Here's how you'd use the client in your isUnique function:

const isSlugUnique = async (value, {client}) => {
  const slugs = await client.fetch(`*[defined(slug)].slug.current`)

  return !slugs.includes(value)
}

//other code 

  {
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'title',
        maxLength: 96,
        isUnique: isSlugUnique,
      },
    },
Awesome! Thank you Racheal you are great!
Aw thank you!
user M
Please can you update the migration cheat sheet for this. There’s still a bunch of old references to v2 , but nothing about how to do this for v3.
Yes, the migration docs needs to be updated.
Where do I put my custom function here, I keep getting a promisify error. Can I run the async function
import { getCliClient } from 'sanity/cli'
const client = getCliClient({ apiVersion: '2021-10-21' })

async function asyncCampaignCodeGenerator(input, context) {
  var chars = 'acdefhiklmnoqrstuvwxyz0123456789'.split('');
  var codeGen = '';
  for (var i = 0; i < 8; i++) {
    var x = Math.floor(Math.random() * chars.length);
    codeGen += chars[x];
  }
  const query = `*[_id == "${input}"]{ "countryCode": jurisdiction->code }`
  const params = { code: input }
  return context.client.fetch(query, params).then(code =>  `${ code[0]?.countryCode || 'G' + '-' + codeGen.toUpperCase()}`)
}
inside the schema file or not?
I am calling it from within a definedField like this:

defineField({
      name: 'code',
      type: 'slug',
      title: 'Campaign Code',
      description: 'This will be the unique campaign code',
      options: {
        source: '_id',
        slugify: asyncCampaignCodeGenerator
      },
      validation: Rule => Rule.required()
    })

is there a fix for this?

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?