
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeYes, 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:
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
}
}
]
}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>
)
}positionDefinition documents using useClient and displays their issueName values as dropdown optionsuseFormValue(['issue']) to access the currently selected issue value from the document, then fetches the corresponding stances from the matching positionDefinitionThe 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.
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.
useClient hook must be called within a React function component, not at the top levelSanity 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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store