Skip to content

[lexical-yjs] Bug Fix: Don't sync root textFormat/textStyle - #8940

Merged
etrepum merged 1 commit into
facebook:mainfrom
patrick-atticus:exclude-formatting
Aug 7, 2026
Merged

[lexical-yjs] Bug Fix: Don't sync root textFormat/textStyle#8940
etrepum merged 1 commit into
facebook:mainfrom
patrick-atticus:exclude-formatting

Conversation

@patrick-atticus

@patrick-atticus patrick-atticus commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

ElementNode.__textFormat and __textStyle are synced for every element, including the root. On the root this produces a stream of pointless updates to the shared document.

The reconciler derives these from the first TextNode of an element's subtree (reconcileTextFormat / reconcileTextStyle), so an empty block remembers the formatting the next typed character inherits.

The root can't be typed into, so the value serves no purpose there. It also never settles: the reconciler sets it from the first text node it meets while rendering, and a partial re-render only covers the part that changed. Loading the document sets the root from the first block, then editing a block further down replaces it with that block's formatting. Every flip is a Yjs update sent to every collaborator, and every client rewrites it on load.

It also isn't limited to users who can edit: reconcileTextFormat is gated on !activeEditorStateReadOnly, not editor.isEditable(), so read-only clients emit these updates too. This was how I found the issue - read-only clients attempting to edit docs and causing errors.

Lexical already treats the value as non-persistable on the root; ElementNode.exportJSON skips it there, and for any element with TextNode children, "Only persist for cases when there are no TextNode children from which these would be set on reconcile (#7968)". This adds the two properties to rootExcludedProperties alongside __cachedText so the binding agrees. The reconciler still sets it locally, and existing documents keep their stored values, just ignored.

Blocks are unchanged: an empty block has no text to derive from, so its value is real state and still syncs.

Related: #7968, #7971

Test plan

No automated test: Collaboration.test.ts can't reproduce it, because activeEditorStateReadOnly is true in that harness so the reconciler never derives the value. There's also no existing coverage of rootExcludedProperties to extend. Guidance welcome.

Verified in a browser. Open a document whose first text run is formatted, then edit a differently formatted block and reload.

Before

doc.get('root', Y.XmlElement).getAttributes()
// { __textFormat: 1, __textStyle: 'color: #00518F;' }

A new update every time the value flips.

After

doc.get('root', Y.XmlElement).getAttributes()
// {}

No updates. The root still holds the value locally, only the sharing is removed. Empty blocks still round-trip their textFormat / textStyle.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview Aug 6, 2026 6:50am
lexical-playground Ready Ready Preview Aug 6, 2026 6:50am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 6, 2026
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Aug 6, 2026
@etrepum
etrepum added this pull request to the merge queue Aug 7, 2026
Merged via the queue into facebook:main with commit 9346ea2 Aug 7, 2026
112 of 116 checks passed
@etrepum etrepum mentioned this pull request Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants