How to lock array items and convert them to fixed fields in Sanity Studio?
I understand what you're trying to achieve! You want editors to initially build a page using a flexible array of modules, but then "lock" that structure so they see individual module fields instead of an array interface. This is a creative approach to combining flexibility during setup with a cleaner editing experience afterward.
The good news is you can accomplish this using conditional fields with the hidden property. However, I need to clarify the architectural approach—Sanity schemas are static, so we can't dynamically create new fields at runtime. Instead, you'll need to define all potential module fields upfront and conditionally show/hide them.
The Approach
The strategy is to:
- Define both the array field AND individual module fields in your schema
- Use a boolean flag to track whether the page is "finalized"
- Use conditional
hiddenproperties to swap which fields are visible - Optionally use
readOnlyto prevent editing of finalized fields
Implementation
Step 1: Schema with conditional visibility
export default {
name: 'page',
type: 'document',
fields: [
{
name: 'title',
type: 'string'
},
{
name: 'isFinalized',
type: 'boolean',
title: 'Structure Finalized',
description: 'Lock the page structure',
initialValue: false
},
// Array shown during initial setup
{
name: 'moduleSetup',
title: 'Page Modules (Setup)',
type: 'array',
of: [
{type: 'fooModule'},
{type: 'barModule'},
{type: 'heroModule'}
],
hidden: ({document}) => document?.isFinalized === true
},
// Individual fields shown after finalization
{
name: 'fooModule',
type: 'fooModule',
hidden: ({document}) => document?.isFinalized !== true,
readOnly: ({document}) => document?.isFinalized === true
},
{
name: 'barModule',
type: 'barModule',
hidden: ({document}) => document?.isFinalized !== true,
readOnly: ({document}) => document?.isFinalized === true
},
{
name: 'heroModule',
type: 'heroModule',
hidden: ({document}) => document?.isFinalized !== true,
readOnly: ({document}) => document?.isFinalized === true
}
]
}Step 2: Create a migration script to transform data
When the editor toggles isFinalized to true, you'll need to manually copy data from the array into the individual fields. You can do this with a migration script:
// migrations/finalizePageStructure.js
import {getCliClient} from 'sanity/cli'
const client = getCliClient()
const query = `*[_type == "page" && isFinalized == true && defined(moduleSetup)]`
client.fetch(query).then(pages => {
const patches = pages.map(page => {
const operations = []
// Copy each array item to its corresponding field
page.moduleSetup?.forEach(module => {
const fieldName = module._type
operations.push({set: {[fieldName]: module}})
})
// Remove the setup array
operations.push({unset: ['moduleSetup']})
return client.patch(page._id).operations(operations).commit()
})
return Promise.all(patches)
})Run this migration after editors mark pages as finalized.
Alternative: Custom Document Action
If you want a more automated approach, you can create a custom document action that performs the transformation when clicked:
// documentActions.js
import {useDocumentOperation} from 'sanity'
import {useCallback} from 'react'
function FinalizePageAction(props) {
const {patch, publish} = useDocumentOperation(props.id, props.type)
const onHandle = useCallback(() => {
const modules = props.draft?.moduleSetup || []
if (modules.length === 0) return
// Build patch operations
const patchOperations = []
modules.forEach(module => {
patchOperations.push({set: {[module._type]: module}})
})
// Execute patch
patch.execute([
...patchOperations,
{set: {isFinalized: true}},
{unset: ['moduleSetup']}
])
if (props.draft && !props.published) {
publish.execute()
}
}, [props.draft, patch, publish])
return {
label: 'Finalize Structure',
onHandle,
disabled: props.published?.isFinalized || !props.draft?.moduleSetup?.length
}
}
export default function resolveDocumentActions(props, context) {
if (props.type === 'page') {
return [...context.defaultResolve(props), FinalizePageAction]
}
return context.defaultResolve(props)
}Register it in sanity.config.js:
import {defineConfig} from 'sanity'
import resolveDocumentActions from './documentActions'
export default defineConfig({
// ... other config
document: {
actions: resolveDocumentActions
}
})Important Considerations
Static schema: You must define all possible module fields in your schema upfront. You can't dynamically create fields based on what's in the array.
Field naming: The
_typeof your array items should match the field names (e.g.,fooModuletype →fooModulefield).One-way transformation: Once finalized, you'd need to manually "unfinalize" by toggling the boolean and potentially recreating the array.
Validation: Consider adding validation to ensure required modules are present before allowing finalization.
This approach gives you the flexibility you want during setup while providing a cleaner interface afterward. Your English was perfectly clear, and this is a smart pattern for a starter template! Good luck with your 5th project!
Show original thread12 replies
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.