Skip to content

Fields named "text" on custom block objects breaks preview reactivity #2167

@fvieira

Description

@fvieira

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:

  1. Skips the isObjectNode branch (line ~370) -- the correct handler that processes value property changes
  2. Enters the isPartialSpanNode branch (line ~471) -- the wrong handler
  3. 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:

  1. Add a quote block to the PTE
  2. Open the block and type in the text field -- preview shows the first character only and freezes
  3. Type in the source field -- preview updates correctly on every keystroke
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions