Disclaimer: I detected the issue in a Sanity Portable Text, that whenever I wrote into the field named "text", the preview would stop working after the first character, and I checked that it happened only to fields named "text". But then I threw Claude Code at Sanity's repo, then this one, until it supposedly found the issue, and the explanation below was written by it and not validated by me, since I don't know the code well enough to do it, just thought it might help (assuming it's right).
Bug: field named text on custom block objects causes set_node updates to be silently dropped
Summary
When a custom object type used as a block in a Portable Text array has a field named text, changes to that field are silently dropped from editor.value after the initial insertion. This causes preview reactivity to break -- the preview shows the first character typed and then freezes. All other field names work correctly.
Root cause
isPartialSpanNode() in packages/editor/src/internal-utils/portable-text-node.ts identifies any object with a string text property as a partial span node, regardless of _type:
export function isPartialSpanNode(node: unknown): node is PartialSpanNode {
return (
typeof node === 'object' &&
node !== null &&
'text' in node &&
typeof node.text === 'string'
)
}
A block object like {_type: "quote", _key: "abc", text: "hello"} in editor.value matches this check, which causes isObjectNode() to return false (since it checks !isPartialSpanNode(node)).
In apply-operation-to-portable-text.ts, the set_node handler then:
- Skips the
isObjectNode branch (line ~370) -- the correct handler that processes value property changes
- Enters the
isPartialSpanNode branch (line ~471) -- the wrong handler
- Skips the
text key explicitly (if (key === 'text') { continue } at line ~475)
Result: updates to the text field on block objects are silently dropped from editor.value. Since render.element.tsx reads block object values from editor.value, the render callback receives stale data.
Reproduction
Minimal schema:
const quote = defineType({
name: "quote",
type: "object",
fields: [
defineField({
name: "text", // <-- this field name triggers the bug
type: "string",
title: "Quote Text",
}),
defineField({
name: "source", // <-- this field works fine
type: "string",
title: "Source",
}),
],
preview: {
select: {
title: "text",
subtitle: "source",
},
},
})
const body = defineType({
name: "body",
type: "array",
of: [
defineArrayMember({ type: "block" }),
defineArrayMember({ type: "quote" }),
],
})
Steps:
- Add a
quote block to the PTE
- Open the block and type in the
text field -- preview shows the first character only and freezes
- Type in the
source field -- preview updates correctly on every keystroke
- Navigate away from the document and come back -- preview now shows the full
text value
Renaming the field from text to anything else (e.g. quoteText) immediately fixes the issue.
What was ruled out (on the Sanity Studio side)
- Custom PTE input components
- Custom PTE options, decorators, annotations, styles
- Custom preview components
- The preview
select key name (renaming the select key doesn't help -- the actual field name must change)
- Field type (
type: "text" and type: "string" are both affected)
- Other document fields / other PTE members
Suggested fix
isPartialSpanNode should not match nodes that have a _type explicitly set to something other than span. A node with _type: "quote" and text: "hello" is clearly not a span:
export function isPartialSpanNode(node: unknown): node is PartialSpanNode {
return (
typeof node === 'object' &&
node !== null &&
'text' in node &&
typeof node.text === 'string' &&
!('_type' in node && node._type !== 'span')
)
}
This ensures block objects with a text field are correctly identified as object nodes, and their text property updates are processed in the isObjectNode branch of applyOperationToPortableText.
Environment
- Sanity Studio v3 (latest)
@portabletext/editor ^4.3.7
- Affects both
type: "text" and type: "string" field types on the block object
Disclaimer: I detected the issue in a Sanity Portable Text, that whenever I wrote into the field named "text", the preview would stop working after the first character, and I checked that it happened only to fields named "text". But then I threw Claude Code at Sanity's repo, then this one, until it supposedly found the issue, and the explanation below was written by it and not validated by me, since I don't know the code well enough to do it, just thought it might help (assuming it's right).
Bug: field named
texton custom block objects causesset_nodeupdates to be silently droppedSummary
When a custom object type used as a block in a Portable Text array has a field named
text, changes to that field are silently dropped fromeditor.valueafter the initial insertion. This causes preview reactivity to break -- the preview shows the first character typed and then freezes. All other field names work correctly.Root cause
isPartialSpanNode()inpackages/editor/src/internal-utils/portable-text-node.tsidentifies any object with a stringtextproperty as a partial span node, regardless of_type:A block object like
{_type: "quote", _key: "abc", text: "hello"}ineditor.valuematches this check, which causesisObjectNode()to returnfalse(since it checks!isPartialSpanNode(node)).In
apply-operation-to-portable-text.ts, theset_nodehandler then:isObjectNodebranch (line ~370) -- the correct handler that processesvalueproperty changesisPartialSpanNodebranch (line ~471) -- the wrong handlertextkey explicitly (if (key === 'text') { continue }at line ~475)Result: updates to the
textfield on block objects are silently dropped fromeditor.value. Sincerender.element.tsxreads block object values fromeditor.value, the render callback receives stale data.Reproduction
Minimal schema:
Steps:
quoteblock to the PTEtextfield -- preview shows the first character only and freezessourcefield -- preview updates correctly on every keystroketextvalueRenaming the field from
textto anything else (e.g.quoteText) immediately fixes the issue.What was ruled out (on the Sanity Studio side)
selectkey name (renaming the select key doesn't help -- the actual field name must change)type: "text"andtype: "string"are both affected)Suggested fix
isPartialSpanNodeshould not match nodes that have a_typeexplicitly set to something other thanspan. A node with_type: "quote"andtext: "hello"is clearly not a span:This ensures block objects with a
textfield are correctly identified as object nodes, and theirtextproperty updates are processed in theisObjectNodebranch ofapplyOperationToPortableText.Environment
@portabletext/editor^4.3.7type: "text"andtype: "string"field types on the block object