How to update Sanity custom input field value with PatchEvent?

5 replies
Last updated: Jun 16, 2021
Im trying to make a custom input component that takes in a name from the input text field, calls the poke api, then i’m trying to then output the id from that data and then create an image preview of the pokemon from a a url based upon what the ID that is returned is in what I think will be an avatar from the sanity ui (haven’t even got to that bit given I cant get over my first hurdle!!). I created it in the input component as an object using User’s alpha cli , but i’m not entirely sure how I update the values correctly because I don’t fully understand the field patch event. Im not great at explaining so i’ve included some code (and Im sorry if Ive gone about how I do this completely wrong in the first place, Im learning so please forgive me!)
getPokeData(value.pokemonName);

  async function getPokeData(pokemonName) {
    const pokeApi = `<https://pokeapi.co/api/v2/pokemon/${pokemonName}>`;
    const res = await fetch(pokeApi);
    const data = await res.json();
    const pokeId = data.id;
      
  }
above is my code for getting the data, please can someone help explain how I would set the data.id into my field I have created for the ID.
AI Update

Great question! I totally understand the confusion around PatchEvent - it's one of those things that clicks once you see it in action. Let me help you update your Pokemon ID field properly.

The key thing to understand is that in Sanity v3, you don't actually need to import PatchEvent anymore. You can import the patch operations (set, unset, setIfMissing) directly from 'sanity' and pass them to your onChange callback.

Here's how to update your code:

import { set, setIfMissing } from 'sanity';

// In your custom input component
export function PokemonInput(props) {
  const { onChange, value = {} } = props;

  async function getPokeData(pokemonName) {
    const pokeApi = `https://pokeapi.co/api/v2/pokemon/${pokemonName}`;
    const res = await fetch(pokeApi);
    const data = await res.json();
    const pokeId = data.id;
    
    // Update the ID field in your object
    onChange(set(pokeId, ['pokeId'])); // The second argument is the path
  }

  return (
    <div>
      <input
        type="text"
        value={value.pokemonName || ''}
        onChange={(e) => {
          const newName = e.target.value;
          // Update the pokemonName field
          onChange(set(newName, ['pokemonName']));
          // Fetch the Pokemon data when name changes
          if (newName) {
            getPokeData(newName);
          }
        }}
      />
      {value.pokeId && <p>Pokemon ID: {value.pokeId}</p>}
    </div>
  );
}

Key points:

  1. The onChange prop is passed to your component and is what actually updates the document
  2. The set() operation takes two arguments: the value to set, and the path (as an array) to where it should be set
  3. For object fields, use the field name as the path: set(value, ['fieldName'])

If you want to update multiple fields at once (like both pokemonName and pokeId), you can do it in one call by setting the entire object:

onChange(set({
  pokemonName: newName,
  pokeId: pokeId
}));

For your schema, you'd want something like:

{
  name: 'pokemon',
  type: 'object',
  components: {
    input: PokemonInput
  },
  fields: [
    { name: 'pokemonName', type: 'string' },
    { name: 'pokeId', type: 'number' },
    { name: 'imageUrl', type: 'string' }
  ]
}

Once you have the ID working, you can build your image URL (https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokeId}.png) and use the Avatar component from @sanity/ui to display it!

The modern v3 approach is much simpler than the old PatchEvent pattern - just import set, unset, and setIfMissing directly from 'sanity' and pass them to onChange. The onChange callback handles all the real-time synchronization and document updates for you. You can read more about how patches work under the hood if you're curious!

Show original thread
5 replies
Hi User. In order to have your function affect the DOM, one solution would be to use
useEffect()
in React. If we don’t, we could run the fetch all day and the DOM would just say “Yeah, so?”
The biggest problem that comes to my mind is how can we control when the fetch is called? We probably don’t want to use
onChange
since we’d be trying to fetch
p
,
pi
,
pik
,
pika
,
pikac
, and
pikach
before finally fetching
pikachu
. We could add a button to let the user control when to fetch. We could debounce, waiting a certain amount of time until the user stops typing. Probably my favourite would be building in a dropdown that makes suggestions based on what’s typed so far. For simplicity, I used onBlur so when the user clicks or tabs off the input, the fetch is run. Probably not the most intuitive for the user, but anyway…
First, we would import everything needed for the custom input component:


import React, { useState, useEffect } from 'react';
import FormField from 'part:@sanity/components/formfields/default'
import {PatchEvent, set} from 'part:@sanity/form-builder/patch-event'
import {TextInput} from '@sanity/ui'
Then we would set up the component and establish state:


export const UserPokemon = React.forwardRef((props, ref) => {
  const { type, onChange, value } = props;
  const { title, description } = type;
  const [pokeName, setPokeName] = useState();
  const [pokeId, setPokeId] = useState();
We could write a function that does our fetch and sets the results in state:


  async function getPokeData(pokemonName) {
    if(pokemonName === undefined) return;
    const pokeApi = `<https://pokeapi.co/api/v2/pokemon/${pokemonName}>`;
    const res = await fetch(pokeApi);
    const data = await res.json();
    setPokeId(data.id);
    setPokeName(pokeName);
  }
Next we would want to create a function to render out the image:


  function renderPokemon() {
    if(pokeName === undefined || pokeName === '') return;
    return <img src={`<https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokeId}.png>`} alt="Pokemon" />
  }
We use
useEffect
to re-render any time pokeName changes:

  useEffect(() => {
    getPokeData(pokeName)
    renderPokemon()
  }, [pokeName]);
Finally, we render to the studio page and export the component:


  return (
    <>
      <FormField label={title} description={description}>
        <TextInput
          type="string"
          ref={ref}
          value={value}
          onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
          onBlur={event => setPokeName(event.target.value)}
        />
      </FormField>
      <div>
        {renderPokemon()}
      </div>
    </>
  );
})

export default UserPokemon;
To implement this, I would put the code into a file somewhere in your studio (here it is in its entirety):


import React, { useState, useEffect } from 'react';
import FormField from 'part:@sanity/components/formfields/default'
import {PatchEvent, set} from 'part:@sanity/form-builder/patch-event'
import {TextInput} from '@sanity/ui'

export const UserPokemon = React.forwardRef((props, ref) => {
  const { type, onChange, value } = props;
  const { title, description } = type;
  const [pokeName, setPokeName] = useState();
  const [pokeId, setPokeId] = useState();

  async function getPokeData(pokemonName) {
    if(pokemonName === undefined) return;
    const pokeApi = `<https://pokeapi.co/api/v2/pokemon/${pokemonName}>`;
    const res = await fetch(pokeApi);
    const data = await res.json();
    setPokeId(data.id);
    setPokeName(pokeName);
  }

  function renderPokemon() {
    if(pokeName === undefined || pokeName === '') return;
    return <img src={`<https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokeId}.png>`} alt="Pokemon" />
  }

  useEffect(() => {
    getPokeData(pokeName)
    renderPokemon()
  }, [pokeName]);

  return (
    <>
      <FormField label={title} description={description}>
        <TextInput
          type="string"
          ref={ref}
          value={value}
          onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
          onBlur={event => setPokeName(event.target.value)}
        />
      </FormField>
      <div>
        {renderPokemon()}
      </div>
    </>
  );
})

export default UserPokemon;
Then in your schema file, you would import this component from the file and then implement it in your schema using something like:


{
  name: 'someName',
  type: 'string',
  inputComponent: UserPokemon,
}
I might set a height or min-height on the pokemon div so that your fields don’t jump around when re-rendering. There’s also some error handling that could be improved. Hopefully this can get you started and please feel free to follow up with questions.
One more thing—the images are actually returned in
data
so you could probably improve this fetch by just getting that back rather than interpolating
pokeId
into an otherwise hard-coded URL.
Ah what a donut I am. I would have used the usestate and use effect as I’ve done a full Pokédex project with this in react before but I misheard in one of the livestreams saying that use state wasn’t needed and thought the patch thing was used instead so I thought it was meant to be done without the usestate hook 🤦‍♀️ feel really stupid now. Thank you for your help. Will take a proper look tomorrow and redo all my code :)
No, don't feel stupid! There are plenty of ways to get this done and I wouldn't say any are obvious. React asks for a lot to get things done that seem like they should just work—one of the reasons I've been enjoying Svelte lately.
I forgot to say thanks for your help, I got it to work the way you suggested. Im guessing that because the results are generating the div afterwards, there is no way to save to the content lake this way? Been trying all week to get it to work the way I wanted it to so that I could enter the competition and instead i’ve ended up going in circles lol

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?