[lexical] Bug Fix: Cache last-child kind for trailing-<br> reconcile#8548
Merged
etrepum merged 2 commits intoMay 23, 2026
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
approved these changes
May 23, 2026
mayrang
added a commit
to mayrang/lexical
that referenced
this pull request
May 23, 2026
… merge dropped The merge resolution that resolved conflicts via `git checkout --ours` inadvertently reverted main changes that the same files needed. - `LexicalReconciler.ts`: `$reconcileElementTerminatingLineBreak` now reads `prevLineBreak` from `slot.element.__lexicalLastChildKind` (cache) instead of calling `isLastChildLineBreakOrDecorator(prevElement, ...)` on a detached prev-state node — restores the facebook#8548 crash fix. - `LexicalDOMSlot.ts`: `setManagedLineBreak` writes the cache field that the reconciler reads — without this the field declaration was orphan. - `Lexical.js.flow`: migrates remaining `+T` / `+property` to the new `out T` / `readonly` syntax to match facebook#8547's flow migration on main.
etrepum
pushed a commit
to etrepum/lexical
that referenced
this pull request
May 26, 2026
…tate The de-optimized cross-parent path previously hand-walked the prev node map and called `isInline()` on previous-state child instances. For custom nodes whose `isInline()` routes through `getLatest()`, that resolves against the next state and throws once a child has been detached — the same hazard fixed in facebook#8548. It also leaned on the per-instance leaf size cache, which a child changed during the move can invalidate. Instead, read the previous size via `prevEditorState.read(() => node.getTextContentSize())`. Inside that read `getChildren()` / `isInline()` resolve against the previous tree, so we neither touch the forward-mutated DOM cache nor hit the getLatest -> next-state trap, and the size is recomputed from the prev tree rather than from leaf caches. `ElementNode.getTextContentSize()` uses the same double-line-break rule as the reconciler cache, so the value matches `__lexicalTextContent.length`. Bump the cross-parent differential fuzzer back to a seed range that includes a case exercising this path. Co-authored-by: Claude <noreply@anthropic.com> https://claude.ai/code/session_01HYqH94pEhCRhymfZsY2J6e
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
$reconcileElementTerminatingLineBreakdecides whether an element's trailing-<br>shape needs to change by comparing the previous and next render's last-child kind (line-break/decorator/empty/null). Before this PR the "previous" answer came fromisLastChildLineBreakOrDecorator(prevElement, activePrevNodeMap), which callsprevElement.getLastChild().isInline(). For an inlineDecoratorNodewhoseisInline()routes throughgetLatest()(the standard mutable-field idiom — seeEquationNodein the playground), that call resolves the node against the active node map. Once the commit has already removed the decorator (the very case we're trying to reconcile), the key is gone andgetLatest()throws the missing-node invariant.This PR records the last-child kind on the slot DOM element (
__lexicalLastChildKind) alongside everysetManagedLineBreakcall, then reads it on the next reconcile instead of recomputing from the prev node. The slot element is the single chokepoint for trailing<br>shape, so cache writes and reads stay paired by construction. NoisInline()call on a prev-state decorator reference.The DOM-cache shape follows the pattern already in
LexicalPrivateDOM(__lexicalLineBreak,__lexicalTextContent,__lexicalFirstTextKey,__lexicalDir,__lexicalUnmanaged) and matches the maintainer's guidance on a related thread: "anything we need to know about the previous render could be cached in the DOM itself."Follow-up to #8534 (Known limitation: stale
DecoratorNodereference during trailing-<br>reconcile).Test plan
LexicalReconcilerStaleDecorator.test.ts— removes an inlineDecoratorNodeas the last child of a paragraph; before the fix, the reconciler threw the missing-node invariant; after, no errors.pnpm vitest run --project unit— 2594 passed, 0 failed.pnpm tsc --noEmit -p tsconfig.jsonclean.pnpm flowclean.prettier --check/eslintclean on changed files.