Skip to content

fix(core): scope useElement re-renders to the element's own provider - #5325

Merged
zbeyens merged 1 commit into
mainfrom
fix/element-context-fanout
Oct 30, 2026
Merged

zbeyens merged 1 commit into
mainfrom
fix/element-context-fanout

Conversation

@dylans

@dylans dylans commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator
  • Auto release

Why

  • Typing in one table cell re-renders every cell of the table. On a 200 × 10 table that is 2,000 cell renders per keystroke, and the cost grows with table size.
  • Cause: ElementProvider passes { element, entry, path, parent } down through React context, and parent is the ancestor's context value. When the table element changes, the context of every row and cell provider below it changes too.
  • useElement() and usePath() read that context. So every component that calls them re-renders. That includes useTableCellElement, useTableCellElementResizable, useBlockSelected and useCellIndices.

Change

  • Drop the element React context. useElementContext now finds the matching ElementStoreContext (whose value is stable) and subscribes to that provider's runtime store with useSyncExternalStore.
  • ElementProvider stages its new state during render, so children rendered in the same pass read the new element. It publishes the state in a layout effect to consumers that did not re-render.
  • Scope fallback and withElementContext behavior are unchanged; the existing useElementStore specs pass as-is.

Verification

  • New useElement.spec.tsx renders a 3 × 3 table through PlateContent and types into one cell:
    • Only the edited cell re-renders. On main, all 9 re-render.
    • The edited cell's layout effect sees the new text on the first pass. On main, all 9 cells' layout effects run.
  • Mutant: staging state in the layout effect instead of during render fails both tests.
  • bun test packages/core (860) and packages/table (293) pass. @platejs/core typecheck passes.
  • packages/selection fails 36 of 94 locally, the same count on untouched main.
  • Browser check on /docs/table with a 200 × 10 table, where the cell also calls useTableCellElement():
    • One keystroke rendered the cell component 4,000 times on main and 4 times with this change (dev build, StrictMode).

ElementProvider passed each element down through a React context whose value
held its parent's value, so a new table element on every keystroke changed the
context of every row and cell below it. Every useElement or usePath consumer
re-rendered, including all cells of a large table.

useElementContext now subscribes to the matching provider's runtime store with
useSyncExternalStore. ElementProvider stages its new state during render, so
children rendered in the same pass read it, and publishes it in a layout effect
to the consumers that did not re-render.
@codesandbox

codesandbox Bot commented Oct 30, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

@changeset-bot

changeset-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cefda2a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@platejs/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@zbeyens
zbeyens merged commit b60e72a into main Oct 30, 2026
6 checks passed
@zbeyens
zbeyens deleted the fix/element-context-fanout branch October 30, 2026 22:31

This branch was successfully deployed

1 active deployment
Preview — cefda2af Deployed Oct 30, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants