Conditional field values based on another document type in Sanity

8 replies
Last updated: Sep 11, 2022
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
Can they reuse existing
positionsDefinition
?But a question I ask myself is why you need them to be separate docs. But let's think about this later...

In theory this would be possible with some tweaking of the
desk structure
and the
create new
functionality (in the top bar) and using
initialValueTemplates
within the panes, which set the references in
position
to the created
positionsDefinition
.Basically you would create
documentLists
which follow that logic but hide all other functionality to create the "nested"
children
from being accessible in the desk structure.
What is your overall goal with this though? Which part will you reuse how in your front-end ?
🤓
This might not make too much sense unless you know what I mean exactly and it's pretty complex 🫣 so let's not worry about the details and concentrate on the beginning. Give me more information and then we brainstorm 😁
Thanks
user J
! I think your suggestion makes sense. I'll look into some desk structure code samples to understand how to implement this.
To answer your question on why I'd need them to be separate documents, it's because I want the fields in the
position
document to not be a freeform text. At the same time, I don't want to embed the accepted values in the code itself. I'd like something in between, where editors can specify accepted values somewhere. Then use those accepted values.
Ah okay that makes sense ... Kind of like tags right? I will prepare something for you in the next days ( a small instruction, but I need to test first)
Could you in the meantime sketch up a simple Mindmap of how this should functionally look? 🙏
Thanks
user J
I appreciate it! Yeah it's a lot like tags.
Can we wait on that until I gain more clarity on the requirements myself? I'd like to reach back to you at a later time, if that's okay?
Sure, you can tag me then! Happy Weekend 👋
I marked it as solved so nodody else from our team re-reads or gets in here unnecessarily. When you get more insight, just repost here, it will apprear in my inbox then (or tag me in another post)

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?