Skip to content

[lexical] Bug Fix: Refactor RootNode.__cachedText computation for coherency#8099

Merged
etrepum merged 4 commits intofacebook:mainfrom
etrepum:reconciler-cachedText
Jan 31, 2026
Merged

[lexical] Bug Fix: Refactor RootNode.__cachedText computation for coherency#8099
etrepum merged 4 commits intofacebook:mainfrom
etrepum:reconciler-cachedText

Conversation

@etrepum
Copy link
Copy Markdown
Collaborator

@etrepum etrepum commented Jan 28, 2026

Description

The way that the reconciler computes RootNode.__cachedText was inconsistent with the way that ElementNode.getTextContent() works. There were some cache coherency issues with the way ElementNode cached text was handled.

This refactors the logic such that:

  • the text is only accumulated in one variable (previously the subTreeTextContent and editorTextContent were computed independently for no obvious reason)
  • the DOUBLE_LINE_BREAK condition is handled when reconciling children of a node, rather than trying to append it to the cache of the ElementNode itself
  • nodes are only reconciled once (some edge cases could end up processing a node multiple times since there wasn't a continue in the loop when a prevNode is destroyed)

Closes #8096

Test plan

All existing tests pass, and new unit tests pass.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 28, 2026

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

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Jan 30, 2026 0:59am
lexical-playground Ready Ready Preview, Comment Jan 30, 2026 0:59am

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 Jan 28, 2026
@etrepum etrepum changed the title [WIP] [lexical] Breaking Change: Add failing invariant to show reconciler __cachedText inconsistency [lexical] Breaking Change: Refactor RootNode.__cachedText computation for coherency Jan 30, 2026
@etrepum etrepum marked this pull request as ready for review January 30, 2026 01:03
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jan 30, 2026
);
rootNode.append(barParagraphNode, bazParagraphNode);
},
{discrete: true},
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just makes the stack trace nicer when the invariant fails, doesn't change the nature of the test

Comment on lines +41 to +43
expect(root.__cachedText).toBe(
ElementNode.prototype.getTextContent.call(root),
);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cleaner way to do the rootCopy thing, assuming RootNode doesn't do any meaningful changes to its implementation of getTextContent

export interface LexicalPrivateDOM {
__lexicalTextContent?: string | undefined | null;
__lexicalLineBreak?: HTMLBRElement | HTMLImageElement | undefined | null;
__lexicalDirTextContent?: string | undefined | null;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was unused

let subTreeTextContent = '';
let subTreeTextFormat: number | null = null;
let subTreeTextStyle: string | null = null;
let editorTextContent = '';
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is entirely redundant, subTreeTextContent does the same thing


function $createChildren(
children: Array<NodeKey>,
element: ElementNode & LexicalPrivateDOM,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a mistake, LexicalPrivateDOM is for DOM and not Lexical nodes

Comment on lines +552 to +557
if ($isElementNode(prevNode)) {
invariant(
$isElementNode(nextNode),
'Node with key %s changed from ElementNode to !ElementNode',
key,
);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the key must refer to the same exact class of element an invariant makes more sense

return triggerCommandListeners(editor, command, payload);
}

export function $textContentRequiresDoubleLinebreakAtEnd(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All use cases of this function were inlined because the isLastChild condition is known at all call sites

@etrepum etrepum changed the title [lexical] Breaking Change: Refactor RootNode.__cachedText computation for coherency [lexical] Bug Fix: Refactor RootNode.__cachedText computation for coherency Jan 30, 2026
@etrepum etrepum added this pull request to the merge queue Jan 31, 2026
Merged via the queue into facebook:main with commit 6f8b4ff Jan 31, 2026
45 checks passed
@etrepum etrepum deleted the reconciler-cachedText branch January 31, 2026 17:39
@etrepum etrepum mentioned this pull request Jan 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.

Bug: $getRoot().getTextContent() new line inconsistencies

2 participants