Trouble with custom input for ecommerce app, resolved with conditional fields.
I can help you with this! The issue you're experiencing is a common challenge when building variant-style custom inputs in Sanity. The problem is that each TextInput needs to update a specific path in your data structure, not just overwrite the entire value.
The solution is to structure your data as an array of objects where each object represents a variant (a size/media combination), and then use the correct Studio v3 patch operations to update individual items.
Data Structure
First, set up your schema to store variants as an array of objects:
{
name: 'artwork',
type: 'document',
fields: [
{
name: 'sizes',
type: 'array',
of: [{type: 'string'}]
},
{
name: 'media',
type: 'array',
of: [{type: 'string'}]
},
{
name: 'variants',
type: 'array',
of: [{
type: 'object',
fields: [
{name: 'size', type: 'string'},
{name: 'media', type: 'string'},
{name: 'price', type: 'number'}
]
}],
components: {
input: VariantPriceMatrix // Your custom component
}
}
]
}Custom Input Component (Studio v3)
Here's a working example that creates independent inputs for each variant. The critical difference from Studio v2 is that you call onChange() directly with patch operations—not PatchEvent.from():
import {set, setIfMissing, unset} from 'sanity'
import {Stack, TextInput, Text} from '@sanity/ui'
import {useCallback, useEffect} from 'react'
function VariantPriceMatrix(props) {
const {value = [], onChange, schemaType} = props
// Get sizes and media from the document
const sizes = props.document?.sizes || []
const mediaOptions = props.document?.media || []
// Generate all combinations
const combinations = sizes.flatMap(size =>
mediaOptions.map(media => ({size, media}))
)
// Initialize variants array if needed
useEffect(() => {
if (combinations.length > 0 && value.length === 0) {
const initialVariants = combinations.map(combo => ({
_type: 'object',
_key: `${combo.size}-${combo.media}`,
size: combo.size,
media: combo.media,
price: undefined
}))
// Studio v3: call onChange directly with setIfMissing
onChange(setIfMissing(initialVariants))
}
}, [combinations.length, value.length, onChange])
// Update price for a specific variant
const handlePriceChange = useCallback((variantKey, newPrice) => {
const variantIndex = value.findIndex(v => v._key === variantKey)
if (variantIndex === -1) {
// If variant doesn't exist yet, add it
const combo = combinations.find(c => `${c.size}-${c.media}` === variantKey)
if (combo) {
const newVariant = {
_type: 'object',
_key: variantKey,
size: combo.size,
media: combo.media,
price: newPrice ? Number(newPrice) : undefined
}
onChange(setIfMissing([...value, newVariant]))
}
return
}
// Studio v3: call onChange directly with set() using array path
if (newPrice === '') {
onChange(unset([variantIndex, 'price']))
} else {
onChange(set(Number(newPrice), [variantIndex, 'price']))
}
}, [value, onChange, combinations])
// Sync variants when combinations change
useEffect(() => {
if (combinations.length === 0) return
const currentKeys = new Set(value.map(v => v._key))
const neededKeys = new Set(combinations.map(c => `${c.size}-${c.media}`))
// Add missing variants
const toAdd = combinations
.filter(c => !currentKeys.has(`${c.size}-${c.media}`))
.map(combo => ({
_type: 'object',
_key: `${combo.size}-${combo.media}`,
size: combo.size,
media: combo.media,
price: undefined
}))
// Remove variants that no longer match
const toKeep = value.filter(v => neededKeys.has(v._key))
if (toAdd.length > 0 || toKeep.length !== value.length) {
onChange(set([...toKeep, ...toAdd]))
}
}, [combinations, value, onChange])
if (combinations.length === 0) {
return <Text muted>Add sizes and media options first</Text>
}
return (
<Stack space={3}>
{combinations.map(combo => {
const variantKey = `${combo.size}-${combo.media}`
const variant = value.find(v => v._key === variantKey)
const currentPrice = variant?.price?.toString() || ''
return (
<Stack key={variantKey} space={2}>
<Text size={1} weight="semibold">
{combo.size} / {combo.media}
</Text>
<TextInput
value={currentPrice}
onChange={(event) => handlePriceChange(variantKey, event.currentTarget.value)}
placeholder="Price"
type="number"
/>
</Stack>
)
})}
</Stack>
)
}
export default VariantPriceMatrixKey Concepts for Studio v3
The critical part is understanding how custom input components work in Studio v3. Unlike Studio v2, you call onChange() directly with patch operations:
// ✅ Correct for Studio v3
onChange(set(newPrice, [variantIndex, 'price']))
// ❌ Wrong (Studio v2 pattern)
onChange(PatchEvent.from(set(newPrice, [variantIndex, 'price'])))When you call set(newPrice, [variantIndex, 'price']), you're telling Sanity: "In the array at position variantIndex, update the price field to newPrice". This creates independent updates for each variant instead of replacing the entire value.
The _key field is essential for tracking items in the array. Using a combination of size and media (like "large-canvas") ensures each variant has a unique, stable identifier.
Alternative: Object-based Structure
If you prefer simpler path management, you could structure variants as an object with keys for each combination:
{
name: 'variantPrices',
type: 'object',
components: {
input: VariantPriceMatrix
}
}Then update individual prices like:
const handlePriceChange = (variantKey, newPrice) => {
if (newPrice === '') {
onChange(unset([variantKey]))
} else {
onChange(set(Number(newPrice), [variantKey]))
}
}This approach eliminates array index management since you're working with object keys directly. However, arrays give you more flexibility for querying and ordering variants in GROQ.
The custom input components documentation has more details on working with complex data structures, and the guide on understanding custom input components for complex objects shows additional patterns for handling nested data!
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.