
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeI 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.
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
}
}
]
}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 VariantPriceMatrixThe 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.
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 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