Conditional field values based on another document type in Sanity

8 replies
Last updated: Nov 29, 2025
Question about configuring the schemas in the Studio:
I have a document type called
position
to represent a political position of a public figure. The position document has the fields
issue
(the name of the issue, i.e. abortion, gun control, etc), and
stance
, i.e. pro-choice, pro-life, pro-gun control, pro-2nd amendment, etc.
What I would like to do is have the value of
issue
and the value of
stance
to be restricted based on another document type called
positionDefinition
. So,
positionDefinition
would have the fields
issueName
(string) and
stances
(an array of strings). To create a
position
the editor has to first create a
positionDefinition
. Then they can create a
position
document where the
issue
field is one from a
positionDefinition
and the
stance
is one of the stances that were defined for that issue in
positionDefinition
.
Does that make sense, is it possible to do?
AI Update

Yes, this makes perfect sense and is absolutely possible! What you're describing is a dependent field pattern where the available options in one field are determined by the value of another field, and both are constrained by a separate definition document. This is a great use case for custom input components.

Here's how to implement it:

Schema Setup

First, define your schemas:

// positionDefinition.ts
export default {
  name: 'positionDefinition',
  title: 'Position Definition',
  type: 'document',
  fields: [
    {
      name: 'issueName',
      title: 'Issue Name',
      type: 'string',
      validation: Rule => Rule.required()
    },
    {
      name: 'stances',
      title: 'Stances',
      type: 'array',
      of: [{type: 'string'}],
      validation: Rule => Rule.required().min(1)
    }
  ]
}

// position.ts
import { IssueInput } from './components/IssueInput'
import { StanceInput } from './components/StanceInput'

export default {
  name: 'position',
  title: 'Position',
  type: 'document',
  fields: [
    {
      name: 'issue',
      title: 'Issue',
      type: 'string',
      components: {
        input: IssueInput
      }
    },
    {
      name: 'stance',
      title: 'Stance',
      type: 'string',
      components: {
        input: StanceInput
      }
    }
  ]
}

Custom Input Components

Now create the custom components using useFormValue and useClient:

// components/IssueInput.tsx
import { StringInputProps, useClient } from 'sanity'
import { useEffect, useState } from 'react'
import { Select } from '@sanity/ui'

export function IssueInput(props: StringInputProps) {
  const { value, onChange } = props
  const client = useClient({ apiVersion: '2024-01-01' })
  const [issues, setIssues] = useState<Array<{ _id: string; issueName: string }>>([])

  useEffect(() => {
    client
      .fetch('*[_type == "positionDefinition"]{ _id, issueName }')
      .then(setIssues)
  }, [client])

  return (
    <Select
      value={value || ''}
      onChange={(event) => onChange(event.currentTarget.value)}
    >
      <option value="">Select an issue...</option>
      {issues.map((issue) => (
        <option key={issue._id} value={issue.issueName}>
          {issue.issueName}
        </option>
      ))}
    </Select>
  )
}

// components/StanceInput.tsx
import { StringInputProps, useClient, useFormValue } from 'sanity'
import { useEffect, useState } from 'react'
import { Select, Stack, Text } from '@sanity/ui'

export function StanceInput(props: StringInputProps) {
  const { value, onChange } = props
  const client = useClient({ apiVersion: '2024-01-01' })
  
  // Access the current document's issue field using useFormValue
  const selectedIssue = useFormValue(['issue']) as string | undefined
  
  const [stances, setStances] = useState<string[]>([])

  useEffect(() => {
    if (!selectedIssue) {
      setStances([])
      return
    }

    client
      .fetch(
        '*[_type == "positionDefinition" && issueName == $issue][0].stances',
        { issue: selectedIssue }
      )
      .then((result) => setStances(result || []))
  }, [client, selectedIssue])

  if (!selectedIssue) {
    return (
      <Stack space={3}>
        <Text size={1} muted>
          Please select an issue first
        </Text>
      </Stack>
    )
  }

  return (
    <Select
      value={value || ''}
      onChange={(event) => onChange(event.currentTarget.value)}
    >
      <option value="">Select a stance...</option>
      {stances.map((stance) => (
        <option key={stance} value={stance}>
          {stance}
        </option>
      ))}
    </Select>
  )
}

How It Works

  1. IssueInput: Fetches all positionDefinition documents using useClient and displays their issueName values as dropdown options
  2. StanceInput: Uses useFormValue(['issue']) to access the currently selected issue value from the document, then fetches the corresponding stances from the matching positionDefinition
  3. The stance field automatically updates when the issue changes, showing only the relevant stances for that issue

The key here is the useFormValue hook, which replaced the older withDocument HOC in Studio v3. It allows you to access any field value in the current document, enabling the dependent field behavior you need.

Alternative: Using References for Better Data Integrity

Instead of storing the issue as a string, you could use a reference field for even better data integrity:

{
  name: 'issueRef',
  title: 'Issue',
  type: 'reference',
  to: [{type: 'positionDefinition'}]
}

Then in your StanceInput, you'd dereference it:

const issueRef = useFormValue(['issueRef']) as { _ref: string } | undefined

useEffect(() => {
  if (!issueRef?._ref) {
    setStances([])
    return
  }
  
  client
    .fetch('*[_id == $id][0].stances', { id: issueRef._ref })
    .then((result) => setStances(result || []))
}, [client, issueRef])

This approach ensures that if you rename an issue in positionDefinition, all existing position documents automatically reflect the change, and you maintain referential integrity.

Important Notes

  • Make sure to include the dependency array in useEffect to avoid excessive API requests
  • The useClient hook must be called within a React function component, not at the top level
  • If you want to make the stance field conditionally required, check out the conditional fields documentation for validation patterns
Show original thread
8 replies

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?