How to update another field using patch in Sanity custom component?

24 replies
Last updated: Apr 24, 2023
Hi everyone, I need a help with customComponent I am trying to change another field from the same document by
const { patch } = useDocumentOperation(props.id, props.schemaType.name);
patch.execute([{ set: { path: pathSegments, value: 'testValue' } }])
but the value of the target field is not changing, What am I missing?
AI Update

I see your issue! When working in a custom component, you shouldn't use useDocumentOperation to update other fields in the same document. Instead, you need to use PatchEvent with the onChange callback that's passed to your custom component via props.

Here's the correct approach:

import { set, unset } from 'sanity'

// In your custom component
function MyCustomComponent(props) {
  const { onChange } = props
  
  const updateAnotherField = (value) => {
    // Create a patch event targeting the field you want to update
    onChange(set(value, ['fieldName']))
  }
  
  return (
    <button onClick={() => updateAnotherField('testValue')}>
      Update Field
    </button>
  )
}

Key points:

  1. Use onChange from props: Your custom component receives an onChange callback in props. This is what you need to use to update field values.

  2. Path specification: The second argument to set() is the path as an array. For a top-level field, it's just ['fieldName']. For nested fields, it would be ['parent', 'child', 'fieldName'].

  3. Why not useDocumentOperation?: The useDocumentOperation hook is designed for document actions (like custom publish buttons), not for updating fields from within custom input components. It operates at the document level, while onChange with PatchEvent is the proper way to handle field-level updates.

For more complex updates:

If you need to update multiple fields or work with objects:

import { set, setIfMissing } from 'sanity'

// Update a simple field
onChange(set('testValue', ['targetField']))

// Update a nested field in an object
onChange(set('testValue', ['parentObject', 'childField']))

// Ensure field exists before setting
onChange([setIfMissing({}), set('testValue', ['targetField'])])

This approach ensures your changes work properly with Sanity's real-time collaboration features and form state management.

Show original thread
24 replies
similar question to mine! I've just done some digging, and found
useFormValue
in the docs (https://www.sanity.io/docs/studio-react-hooks#5cb6622b6381 )
you can use this as such to get the document ID:

const documentId = useFormValue(["_id"]);
Now, if the document is a draft, it will be prefixed with
draft.
So that needs to be removed in order to pass it into
useDocumentOperation

e.g:

const documentId = useFormValue(["_id"]);

const docId = typeof documentId === "string"
  ? documentId.replace("drafts.", "")
  : props.id;
Had to do a
typeof
in front of the replace in my case, as Typescript kept complaining it was a Type
unknown
, and I'm still relatively new to TS in general!
props.id
in this case returns the name of the field that you set in your schema, so ultimately I think it doesn't do anything to update the document as a whole. Otherwise would be a lot easier!
useFormValue
is ok when I need to change current field, in my case I need to change different field
here is the example what I am building
I want ‘Translate All’ fill all non-english terms to be changed
Yeah, the
useFormValue
allows you to get the document ID to update different fields in the same document
indeed, last question why its not setting new value
patch.execute([{ 'set': { path: pathSegments, value: 'testValue' } }])
so I have
 const { patch } = useDocumentOperation(docId!, props.schemaType.name);

  //<https://translation.googleapis.com/language/translate/v2?q=super> puper text that needs translation&target=uk&format=text&source=en&model=base&key=
  async function translateText() {

    if (source !== undefined || source !== 'undefined' || source !== '' || source !== null) {

      const res = await <http://axios.post|axios.post><TranslateRespose>(
        `<https://translation.googleapis.com/language/translate/v2?q=${source}&target=${locale}&format=text&source=en&model=base&key=${key}>`
      );

      const translatedText = res.data.data.translations[0].translatedText;

      onChange(PatchEvent.from(set(translatedText)))
    }
  }

  async function translateAllChildrenText() {
    languages.map(async (lang) => {
      var pathSegments = [...props.path];
      pathSegments[pathSegments.length - 1] = lang.id;

      patch.execute([{ 'set': { path: pathSegments, value: 'testValue' } }])
    })
  }

The set object should look something like this:
patch.execute([
  {
    set: {
      SCHEMA_FIELD_NAME: NEW_VALUE,
    }
  }
])
Not sure what your schema fields are called, so try setting
SCHEMA_FIELD_NAME: 'testValue'
sorry for stupid question but I have this structure
I was trying to use SCHEMA_FILED_NAME `name.uk
name[uk]
but its not settings up values
can you share the document schema you're updating?
sure
import {MdLibraryMusic as icon} from 'react-icons/md'
import {MdYoutubeSearchedFor as seoIcon} from 'react-icons/md'
import {defineField, defineType} from 'sanity'
import DurationInputComponent from '../components/DurationInputComponent'
import TranslateStringInputComponent from '../components/TranslateStringInputComponent'

export default defineType({
  name: 'track',
  title: 'Track',
  type: 'document',
  icon,
  groups: [
    {
      name: 'seo',
      title: 'SEO',
      icon: seoIcon,
    },
    {
      name: 'codes',
      title: 'Codes',
    },
    {
      name: 'metadata',
      title: 'Metadata',
    },
    {
      name: 'statistics',
      title: 'Stats',
    },
  ],
  fields: [
    defineField({
      name: 'name',
      title: 'Name',
      type: 'localeString',
      group: 'metadata',
      options: {
        languageFilter: true,
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'description',
      title: 'description',
      type: 'localeString',
      group: 'metadata',
      options: {
        languageFilter: true,
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'isFeatured',
      title: 'Is Featured',
      type: 'boolean',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'tags',
      title: 'Tags',
      type: 'array',
      // validation: (Rule) => Rule.required(),
      of: [{type: 'reference', to: [{type: 'searchTag'}]}],
    }),
    defineField({
      name: 'image',
      title: 'Poster Image',
      type: 'image',
      group: 'metadata',
      validation: (Rule) => Rule.required(),
      options: {
        hotspot: true,
      },
    }),
    defineField({
      name: 'file',
      title: 'File',
      type: 'file',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'name',
        maxLength: 100,
      },
    }),
    defineField({
      name: 'tempo',
      title: 'Tempo',
      type: 'number',
      group: 'metadata',
      initialValue: 100,
      validation: (Rule) =>
        Rule.precision(0).positive() && Rule.max(200) && Rule.min(50) && Rule.required(),
    }),
    defineField({
      name: 'duration',
      title: 'Duration (seconds)',
      type: 'number',
      group: 'metadata',
      readOnly: true,
      components: {
        input: DurationInputComponent,
      },
    }),
    defineField({
      name: 'visitCount',
      title: 'Visit',
      type: 'number',
      group: 'statistics',
    }),
    defineField({
      name: 'playCount',
      title: 'Play',
      type: 'number',
      group: 'statistics',
    }),
    defineField({
      name: 'downloadCount',
      title: 'Download',
      type: 'number',
      group: 'statistics',
    }),
    defineField({
      name: 'buyCount',
      title: 'Buy',
      type: 'number',
      group: 'statistics',
    }),
    defineField({
      name: 'upc',
      title: 'UPC',
      type: 'string',
      validation: (Rule) => Rule.required(),
      group: 'codes',
    }),
    defineField({
      name: 'isrc',
      title: 'ISRC',
      type: 'string',
      validation: (Rule) => Rule.required(),
      group: 'codes',
    }),
    defineField({
      name: 'iswc',
      title: 'ISWC',
      type: 'string',
      validation: (Rule) => Rule.required(),
      group: 'codes',
    }),
    defineField({
      name: 'recordLabel',
      title: 'Record Label',
      type: 'string',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'artists',
      title: 'Artists',
      type: 'array',
      validation: (Rule) => Rule.required(),
      of: [{type: 'reference', to: [{type: 'artist'}]}],
    }),
    defineField({
      name: 'composers',
      title: 'Composers',
      type: 'array',
      validation: (Rule) => Rule.required(),
      of: [{type: 'reference', to: [{type: 'composer'}]}],
    }),
    defineField({
      name: 'sources',
      title: 'Sources',
      type: 'array',
      of: [{type: 'streamingService'}],
    }),
    defineField({
      name: 'moods',
      title: 'Moods',
      type: 'array',
      validation: (Rule) => Rule.required(),
      of: [{type: 'reference', to: [{type: 'mood'}]}],
    }),
    defineField({
      name: 'ganres',
      title: 'Ganres',
      type: 'array',
      validation: (Rule) => Rule.required(),
      of: [{type: 'reference', to: [{type: 'ganre'}]}],
    }),
    defineField({
      name: 'seo',
      title: 'SEO',
      type: 'pageSeo',
      group: 'seo',
      // validation: (Rule) => Rule.required(),
    }),
  ],
  preview: {
    select: {
      name: 'name',
      media: 'image',
      isFeatured: 'isFeatured',
    },
    prepare(selection) {
      const {name, media, isFeatured} = selection
      return {
        title: name['en'],
        media,
        subtitle: isFeatured ? '⭐️ Featured' : '',
      }
    },
  },
  initialValue: {
    isFeatured: false,
    recordLabel: 'lesfreemusic',
    sources: [
      {name: 'Patreon', icon: 'mdi:patreon'},
      {name: 'Spotify', icon: 'mdi:spotify'},
      {name: 'Apple Music', icon: 'cib:apple-music'},
      {name: 'iTunes', icon: 'simple-icons:itunes'},
      {name: 'Youtube', icon: 'uil:youtube'},
      {name: 'Youtube Music', icon: 'arcticons:youtube-music'},
      {name: 'Amazon Music', icon: 'arcticons:amazon-music'},
      {name: 'Amazon', icon: 'simple-icons:amazon'},
      {name: 'TikTok', icon: 'ic:baseline-tiktok'},
      {name: 'Instagram', icon: 'mdi:instagram'},
      {name: 'SoundCloud', icon: 'mdi:soundcloud'},
      {name: 'Deezer', icon: 'tabler:brand-deezer'},
    ],
    composers: [{_ref: '45688ba9-a315-400b-98b3-b8c1442b9aa4'}],
  },
})
import {defineType} from 'sanity'
import TranslateStringInputComponent from '../../components/TranslateStringInputComponent'
import {languages} from '../../supportedLanguages'

// const languages = [
//   {id: 'en', title: 'English', isDefault: true},
//   {id: 'uk', title: 'Ukrainian'},
//   {id: 'cs', title: 'Czech'},
//   {id: 'da', title: 'Danish'},
//   {id: 'de', title: 'German'},
//   {id: 'es', title: 'Spanish'},
//   {id: 'fr', title: 'French'},
//   {id: 'it', title: 'Italian'},
//   {id: 'hu', title: 'Hungarian'},
//   {id: 'nl', title: 'Dutch'},
//   {id: 'pl', title: 'Polish'},
//   {id: 'pt', title: 'Portuguese'},
//   {id: 'pt_BR', title: 'Portuguese (Brazil)'},
//   {id: 'fi', title: 'Finnish'},
//   {id: 'sv', title: 'Swedish'},
//   {id: 'tr', title: 'Turkish'},
//   {id: 'ru', title: 'Russian'},
//   {id: 'th', title: 'Thai'},
//   {id: 'ko', title: 'Korean'},
//   {id: 'ja', title: 'Japanese'},
//   {id: 'zh_CN', title: 'Chinese (Simplified)'},
//   {id: 'zh_TW', title: 'Chinese (Traditional)'},
//   {id: 'ar', title: 'Arabic'},
//   {id: 'he', title: 'Hebrew'},
//   {id: 'hi', title: 'Hindi'},
//   {id: 'id', title: 'Indonesian'},
//   {id: 'fil', title: 'Filipino'},
//   {id: 'vi', title: 'Vietnamese'},
// ]

export default defineType({
  name: 'localeString',
  title: 'Locale String',
  type: 'object',
  fieldsets: [
    {
      title: 'Translations',
      name: 'translations',
      options: {collapsible: true, collapsed: true},
    },
  ],
  // @ts-ignore
  fields: languages.map((lang) => ({
    name: lang.id,
    title: lang.title,
    type: 'string', // or `text`, etc
    fieldset: lang.isDefault ? null : 'translations',
    components: {
      input: TranslateStringInputComponent,
    },
  })),
})`
Just to make sure I'm reading that right, only name and description are using your localeString type, and that's where the translations should be populating?
yes
so I'm guessing a little bit here, would the following work?
patch.execute([
  {
    set: {
      name: {
        uk: 'test value',
      }
    }
  }
])

you made my day, if you need music for videos let me know I will give you a license for using over 900 tracks from my library
awesome, glad it worked out! 😄
user H
be my saver today I cant apply all patches I am trying to execute this
 const translations = await Promise.all(languages.map(async (lang) => {

      var pathSegments = [...props.path];
      pathSegments[pathSegments.length - 1] = lang.id;
      const path = pathSegments.join('.');

      const translation = lang.id === 'en' ? source : await translate(source, `${lang.id}`);

      console.log('test: ', path);

      const patch = {
        set: {
          name: {
            [lang.id]: translation
          }
        }
      };

      patches.push(patch);
      return translation;
    }));

    console.log('translations: ', translations);
    console.log('patches: ', patches);

    const res = patch.execute(patches)
even when I am trying to do this
const res = patch.execute([
      {
        set: {
          name: {
            uk: 'ukr'
          }
        }
      },
      {
        set: {
          name: {
            en: source
          }
        }
      },
      {
        set: {
          name: {
            de: 'de value'
          }
        }
      },
    ])
I’ve got only de filed updated and en is earased
I guess it would depend how you're doing the translations? Does it only translate one file at a time?
I'd say maybe populate and object as follows:

const nameTranslation = {
  uk: 'uk name',
  en: 'en name',
  de: 'de name',
}
and then run your set as:

set: {
  name: nameTranslation
}
and see if that works to update multiple fields?

Then you could populate the object with all the keys + translations, and pass it through as one patch?

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?