How to pass field values from schema into custom Studio component?

12 replies
Last updated: Aug 23, 2023
I’ve got a custom component loaded into Studio. It belongs to a post schema. I’m have trouble figuring out how to pass data from the other fields into it. For example I might want to utilize the values of title, except, and body, in it. It’s essentially a hidden form from which I’m going to build a json structure to fire off to the outside world.
AI Update

Perfect! The useFormValue hook is exactly what you need to access values from other fields in your custom component. Here's how to do it:

import { useFormValue } from 'sanity'

const MyCustomComponent = (props) => {
  // Access individual fields from your post schema
  const title = useFormValue(['title'])
  const excerpt = useFormValue(['excerpt'])
  const body = useFormValue(['body'])
  
  // Or get the entire document at once
  const entireDocument = useFormValue([])
  
  // Build your JSON structure
  const jsonStructure = {
    title: title,
    excerpt: excerpt,
    body: body,
    // ... other fields you need
  }
  
  // Your logic to fire off to external world
  const handleSubmit = () => {
    // Send jsonStructure wherever you need
  }
  
  return (
    // Your hidden form UI
  )
}

The key points:

Path syntax: Pass an array to useFormValue with the field name(s):

  • useFormValue(['title']) - gets the title field
  • useFormValue(['parent', 'child']) - for nested fields
  • useFormValue([]) - gets the entire document

Dynamic updates: The hook automatically re-renders your component when those field values change, so your JSON structure will always reflect the current state of the form.

Context requirement: This only works inside custom input components in the form builder. You can't use it in document actions or outside the Studio form context.

If you migrated from Studio v2, this replaces the old props.document pattern - in v3 you must use the useFormValue hook instead.

For your hidden form use case, you might want to use useFormValue([]) to grab the whole document at once, then extract just the fields you need when building your JSON to send externally. This way you don't need to call the hook multiple times if you're working with many fields.

Show original thread
12 replies
There is a helper function called
useFormValue()

import {useFormValue} from 'sanity'
const form = useFormValue([])
omg I should have asked you three hours ago lol. Thank you! haha I was exploring all the wrong rabbit holes.
I really appreciate it.
Only three hours? That's not bad. I've been spending 300 hours with
preview-kit
😉
haha
I’m having a hell of a time passing values from useFormValue to an api route. I have a component that is hitting the endpoint and firing off the email but I can’t seem to get any of the values through. I have undefined values across the board it seems, except when logging the values I do in fact seem them in the console.
I’m having a hell of a time passing values from useFormValue to an api route. I have a component that I am loading into my post editor that is hitting the endpoint and firing off the email but I can’t seem to get any of the values through. I have undefined values across the board it seems, except when logging the values I do in fact seem them in the console. They are there, but they are not there. I can print them out in the component for example, but they are not really there. 😞


// lib/sanity/schemas/SendEmail.jsx
import { toHTML } from '@portabletext/to-html';
import React from 'react';
import { useFormValue } from 'sanity';

const components = {
  types: {
    image: ({ value }) => {
      if (value && value.asset && value.asset._ref) {
        return `<img src="${value.asset._ref}" />`;
      } else {
        return '';
      }
    },
    embed: ({ value }) => {
      if (value && value.url) {
        return `<iframe src="${value.url}" />`;
      } else {
        return '';
      }
    }
  }
};

export default function SendEmail(props) {
  const form = useFormValue([]);
  const [title] = useFormValue(['title']);
  const [body] = useFormValue(['body']);
  const [section] = useFormValue(['section']);
  const [subTitle] = useFormValue(['excerpt']);
  const bodyHtml = toHTML(body, { components });

  const authorName = 'Author Name';
  const authorSlug = 'author-slug';
  const authorImageUrl = 'author-image';

  const handleSendEmail = async () => {
    // Prepare the request body
    const requestBody = {
      title: title[0],
      subTitle: subTitle[0],
      section: section[0]
    };

    try {
      const response = await fetch('/api/send-email', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestBody)
      });

      if (response.ok) {
        setEmailSent(true);
      } else {
        console.error('Email sending failed');
      }
    } catch (error) {
      console.error('An error occurred:', error);
    }
  };

  return (
    <div className="space-y-2 border p-4">
      <h2 className="text-xl font-bold">Send Email</h2>
      <div className="flex flex-col space-y-2">
        <button onClick={handleSendEmail}>Send Email</button>
      </div>
    </div>
  );
}
and then in the route.ts


// app/api/send-email/route.ts

import { render } from '@react-email/render';
import NewsletterEmail, { NewsletterEmailProps } from 'emails/newsletter';
import { NextApiRequest, NextApiResponse } from 'next';
import { Client } from 'postmark';

const postmarkClient = new Client(process.env.POSTMARK_API_KEY || '');

export async function sendEmailHandler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'POST') {
    try {
      const { title, subTitle, section } = req.body as NewsletterEmailProps;

      console.log('Received request to send email:', {
        title,
        subTitle,
        section
      });

      const emailHtml = render(
        NewsletterEmail({
          title,
          subTitle,
          section
        })
      );

      console.log('Generated email HTML:', emailHtml);

      const response = await postmarkClient.sendEmail({
        From: '<mailto:newsletter@redacted-domain.us|newsletter@redacted-domain.us>',
        To: '<mailto:reader@redacted-domain.us|reader@redacted-domain.us>',
        Subject: 'Newsletter',
        HtmlBody: emailHtml
      });

      console.log('Email sent successfully:', response);

      if (response.ErrorCode) {
        console.error('Error sending email:', response.Message);
        return res.status(500).json({ error: 'Failed to send email' });
      }

      return res.status(200).json({ message: 'Email sent!' });
    } catch (error) {
      console.error('Error sending email:', error);
      return res.status(500).json({ error: 'Failed to send email' });
    }
  } else {
    return res.status(405).json({ error: 'Method not allowed' });
  }
}

export { sendEmailHandler as POST };
When you're referring to things being undefined but logging, is my interpretation correct that everything works including the logs in the API route but just the email itself, as-delivered, doesn't contain anything where the values are expected to show?
Actually the logs in the API route are showing undefined. I have not hooked up those values to the email template. I just have hard-coded the bare minimum as needed in the route to both send via postmarkapp api as well as draw from the email template - these two things work great. So, I’m attempting to get everything declared correctly from the sendEmail component and then logging on the API route to check that I am indeed able to capture the values from the useFormValue.
Been toying with useMemo and useState to no avail. Here is the present sendEmail component:


// lib/sanity/schemas/SendEmail.jsx
import { toHTML } from '@portabletext/to-html';
import React from 'react';
import { useMemo } from 'react';
import { useFormValue } from 'sanity';


const components = {
  types: {
    image: ({ value }) => {
      if (value && value.asset && value.asset._ref) {
        return `<img src="${value.asset._ref}" />`;
      } else {
        return '';
      }
    },
    embed: ({ value }) => {
      if (value && value.url) {
        return `<iframe src="${value.url}" />`;
      } else {
        return '';
      }
    }
  }
};

export default function SendEmail(props) {
  const form = useFormValue([]);
  const title = useFormValue(['title']);
  const section = useFormValue(['section']);
  const subTitle = useFormValue(['excerpt']);
  const preface = useFormValue(['preface']);
  const body = useFormValue(['body']);
  const bodyHtml = toHTML(body, { components });

  // const authorName = 'Author Name';
  // const authorSlug = 'author-slug';
  // const authorImageUrl = 'author-image';

  console.log('form', form);
  console.log('title', title);
  console.log('section', section);
  console.log('subTitle', subTitle);
  console.log('preface', preface);
  console.log('bodyHtml', bodyHtml);

  const [formValues, setFormValues] = React.useState({
    title: '',
    section: '',
    subTitle: '',
    preface: '',
    bodyHtml: ''
  });

  React.useEffect(() => {
    setFormValues({
      title: title,
      section: section,
      subTitle: subTitle,
      preface: preface,
      bodyHtml: bodyHtml
    });
  }, [title, section, subTitle, preface, bodyHtml]);

  const handleSendEmail = async () => {
    // Prepare the request body
    // I only want to send the title, section, and subTitle right now
    const requestBody = {
      title: formValues.title,
      subTitle: formValues.subTitle,
      section: formValues.section
    };

    console.log('Request body:', requestBody);

    try {
      const response = await fetch('/api/send-email', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestBody)
      });

      if (response.ok) {
        setEmailSent(true);
      } else {
        console.error('Email sending failed');
      }
    } catch (error) {
      console.error('An error occurred:', error);
    }
  };

  return (
    <div className="space-y-2 border p-4">
      <h2 className="text-xl font-bold">Send Email</h2>
      <div className="flex flex-col space-y-2">
        <pre>{title}</pre>
        <button onClick={handleSendEmail}>Send Email</button>
      </div>
    </div>
  );
}
I apologize if I sound obtuse but at what point before sending can you no longer see the values? You mentioned they show in the component so I'm assuming they're able to be pulled from the hook.
Does it get as far as them for sure being in the requestBody on the front end before being sent?
Not obtuse at all. I made a quick screen-share and posted the offending files in a commit. Not sure how else to describe the situation.vid:
https://www.loom.com/share/f2a33f455fec405b85c9a04250e6007e?sid=091352b7-7502-4c86-833e-0422e578fcfd gh:
https://github.com/Pirate-Wires/pirate-wires/commit/c09c58d3e6a1d1ef54de4605534af3d3a9c55f08
And yes they appear to be in the requestBody on the front end. Perhaps not being handled correctly when sending or being received correctly. It may be more of a API issue at this point as the useFormValue work in the context of Sanity Studio appears to be good.

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?