Skip to content

[lexical] Bug Fix: Cache last-child kind for trailing-<br> reconcile#8548

Merged
etrepum merged 2 commits into
facebook:mainfrom
mayrang:fix/reconciler-trailing-br-dom-cache
May 23, 2026
Merged

[lexical] Bug Fix: Cache last-child kind for trailing-<br> reconcile#8548
etrepum merged 2 commits into
facebook:mainfrom
mayrang:fix/reconciler-trailing-br-dom-cache

Conversation

@mayrang

@mayrang mayrang commented May 23, 2026

Copy link
Copy Markdown
Contributor

Description

$reconcileElementTerminatingLineBreak decides 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 from isLastChildLineBreakOrDecorator(prevElement, activePrevNodeMap), which calls prevElement.getLastChild().isInline(). For an inline DecoratorNode whose isInline() routes through getLatest() (the standard mutable-field idiom — see EquationNode in 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 and getLatest() throws the missing-node invariant.

This PR records the last-child kind on the slot DOM element (__lexicalLastChildKind) alongside every setManagedLineBreak call, 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. No isInline() 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 DecoratorNode reference during trailing-<br> reconcile).

Test plan

  • New unit test LexicalReconcilerStaleDecorator.test.ts — removes an inline DecoratorNode as 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.json clean.
  • pnpm flow clean.
  • prettier --check / eslint clean on changed files.
  • Playground manual: paste a KaTeX equation, delete it as the last child of a paragraph (the original repro path from [lexical-playground] Bug Fix: EquationNode click → NodeSelection + empty-input Backspace removes #8534) — no console error.

@vercel

vercel Bot commented May 23, 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, Comment May 23, 2026 5:37am
lexical-playground Ready Ready Preview, Comment May 23, 2026 5:37am

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 May 23, 2026
Comment thread packages/lexical/src/LexicalReconciler.ts Outdated
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label May 23, 2026
@etrepum etrepum added this pull request to the merge queue May 23, 2026
Merged via the queue into facebook:main with commit 6ced777 May 23, 2026
45 checks passed
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
@etrepum etrepum mentioned this pull request May 28, 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