How to build a custom input component that dynamically displays a list of checkboxes based on the current value of a separate field.
Yes, absolutely! This is a great use case for a custom input component with useFormValue. Here's how to build it:
Basic Structure
Your custom input component needs to:
- Use
useFormValueto watch the meal field - React to changes with
useEffect - Use
onChangewithset()andunset()to patch the Content Lake
Here's a complete example for your shopping list scenario:
import { useFormValue, set, unset } from 'sanity'
import { useEffect, useState, useCallback } from 'react'
import { Stack, Checkbox, Card } from '@sanity/ui'
export const ShoppingListInput = (props) => {
const { onChange, value = [] } = props
const mealRef = useFormValue(['meal']) // Watch the meal field
const [ingredients, setIngredients] = useState([])
// Fetch ingredients when meal changes
useEffect(() => {
if (mealRef?._ref) {
// Query Sanity for the meal's ingredients
client.fetch(
`*[_id == $mealId][0].ingredients`,
{ mealId: mealRef._ref }
).then(setIngredients)
} else {
setIngredients([])
}
}, [mealRef?._ref]) // Re-run when meal reference changes
const handleCheckboxChange = useCallback((ingredient, checked) => {
let newValue
if (checked) {
// Add ingredient to array
newValue = [...value, ingredient]
} else {
// Remove ingredient from array
newValue = value.filter(item => item._key !== ingredient._key)
}
// Patch to Content Lake - correct v3 pattern
onChange(newValue.length > 0 ? set(newValue) : unset())
}, [onChange, value])
return (
<Stack space={3}>
{ingredients.map((ingredient) => (
<Card key={ingredient._key} padding={2}>
<Checkbox
checked={value.some(v => v._key === ingredient._key)}
onChange={(e) => handleCheckboxChange(ingredient, e.target.checked)}
>
{ingredient.name}
</Checkbox>
</Card>
))}
</Stack>
)
}Key Points
1. The useFormValue hook watches other fields in your document. Pass it an array path like ['meal'] for a top-level field, or ['parent', 'child'] for nested fields.
2. The dependency array in useEffect should include mealRef?._ref (not just mealRef) to trigger updates when the actual reference changes, not just the object identity.
3. Patching with onChange (Studio v3): As shown in the official guide, you pass the result of set() or unset() directly to onChange():
onChange(nextValue ? set(nextValue) : unset())Important: In Studio v3, you do NOT use PatchEvent.from() - that was the v2 pattern. The v3 API is simpler and cleaner.
4. Performance optimization: Wrap your change handler in useCallback with onChange and value in the dependency array to prevent unnecessary re-renders.
Schema Setup
In your schema, attach the component like this:
{
name: 'shoppingList',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'ingredient' }] }],
components: {
input: ShoppingListInput
}
}Common Gotchas
- Context requirement:
useFormValueonly works inside custom input components, not in document actions or other contexts - Real-time updates: The hook automatically re-renders when the watched field changes
- Array handling: Make sure each ingredient has a
_keyfor proper React rendering and array patching - Import from 'sanity': Both
setandunsetare imported from the'sanity'package in v3
This pattern works great for dependent fields, conditional inputs, and dynamic lists based on other selections!
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.