Skip to content

refactor: vendor @tanstack/store as a first-party Store - #2956

Merged
nperez0111 merged 2 commits into
mainfrom
feat/tanstack-store-upgrade-eval
Aug 11, 2026
Merged

refactor: vendor @tanstack/store as a first-party Store#2956
nperez0111 merged 2 commits into
mainfrom
feat/tanstack-store-upgrade-eval

Conversation

@nperez0111

@nperez0111 nperez0111 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the @tanstack/store and @tanstack/react-store dependencies with a ~50-line first-party Store in @blocknote/core and a matching useStore hook in @blocknote/react.

Rationale

We only ever used state, prevState, setState, subscribe and the onUpdate option — never the Derived/Effect reactive graph that makes up the rest of the library, which also doesn't tree-shake because setState reaches into the scheduler that imports Derived. Since Store is unavoidably part of our published API (every extension exposes one, and the vanilla-JS docs pointed users at tanstack.com/store), every breaking change in a 0.x dependency became a BlockNote breaking change — and upstream shipped four minor bumps in eleven months, including a full rewrite onto alien-signals in 0.9.0 and a React hook replacement in 0.11.0, with no 1.0 on the roadmap.

Changes

  • Add packages/core/src/util/Store.ts, exported by name so consumers can import the type directly rather than only reaching it structurally. Includes the re-entrancy guard that lets a listener write back to the store (e.g. one dispatching a ProseMirror transaction) without recursing.
  • Add packages/react/src/hooks/useStore.ts, wrapping the use-sync-external-store shim @blocknote/react already depends on, and preserving the shallow default comparator that useCommentUsers/useVersionUsers rely on.
  • onUpdate now receives the new and previous state, so callers no longer close over the store to read prevState.
  • Drop the TUpdater type parameter, which existed only to serve the unused updateFn option.
  • Remove both dependencies and update the vanilla-JS docs.

Impact

No behavioural change and no public API break: subscribe still returns an unsubscribe function, setState still accepts a value or an updater, and the listener payload is unchanged. Emitted declarations no longer reference @tanstack/store at all (previously 19+ references) and simplify from Store<T, (cb: T) => T> to Store<T>. This also closes a version-skew hazard where core allowed ^0.7.7 to float while react pinned 0.7.7 exactly, so a future 0.7.x publish would have installed two copies with Store instances crossing the package boundary. Bundle cost drops from 1128 B to 340 B gzipped.

Testing

New packages/core/src/util/Store.test.ts covers value- and function-form setState, prevState, onUpdate ordering and arguments, unsubscribe, and re-entrant writes. Full unit suite, lint, type-check and build all pass; e2e passes except for three failures that reproduce identically on a clean tree (see notes).

Screenshots/Video

N/A — no user-visible change.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

keyboardhandlers.test.tsx (2 snapshot mismatches) and multicolumnDrop.test.tsx (a RangeError: Position 30 outside of fragment in multiColumnHandleDropPlugin.ts) fail on this branch, but they fail identically with these changes stashed, so they are pre-existing and unrelated to this PR.

If we ever want Derived-style primitives, the suggested path is to depend on alien-signals directly — it is past 1.0, has zero runtime dependencies, and is the same engine @tanstack/store itself rewrote onto — rather than re-adopting a 0.x store wrapper.

Summary by CodeRabbit

  • New Features

    • Added a lightweight store API for managing and subscribing to application state.
    • Exposed store utilities and the React useStore hook through the public packages.
    • Added state selection with shallow comparison to help prevent unnecessary React updates.
  • Bug Fixes

    • Improved notification handling for consecutive and nested state updates.
    • Preserved comment thread selection updates when state changes.
  • Documentation

    • Updated vanilla JavaScript guidance with direct instructions for using the store API.

BlockNote used only a fraction of @tanstack/store: `state`, `prevState`,
`setState`, `subscribe`, and the `onUpdate` option. The library's actual
value-add — the `Derived`/`Effect` reactive graph — was never used, and it
doesn't tree-shake, since `setState` reaches into the scheduler which imports
`Derived` at module top level.

The `Store` type is also unavoidably part of BlockNote's published API: every
extension exposes one, so the emitted declarations carried 19+
`import("@tanstack/store").Store<...>` references, and the vanilla-JS docs
pointed users at tanstack.com/store. That meant every breaking change in a 0.x
dependency became a BlockNote breaking change — and upstream has shipped four
minor bumps in eleven months, including a full rewrite onto alien-signals
(0.9.0) and a React hook replacement (0.11.0), with no 1.0 on the roadmap.

Vendoring the ~50 lines we actually use keeps that public surface stable and
under our control. Behaviour is unchanged, including the re-entrancy guard that
lets a listener write back to the store (e.g. one dispatching a ProseMirror
transaction) without recursing.

- Add `packages/core/src/util/Store.ts` and export `Store` by name, so consumers
  can import the type directly instead of only reaching it structurally.
- Add `packages/react/src/hooks/useStore.ts`, wrapping the
  `use-sync-external-store` shim that `@blocknote/react` already depends on.
  Keeps the `shallow` default comparator that `useCommentUsers` and
  `useVersionUsers` rely on to avoid re-rendering on unrelated user updates.
- Give `onUpdate` the new and previous state, so callers no longer close over
  the store to read `prevState`.
- Drop the `TUpdater` type parameter, which only existed to serve the unused
  `updateFn` option. Emitted types simplify from `Store<T, (cb: T) => T>` to
  `Store<T>`.
- Remove both dependencies. This also closes a version-skew hazard: core allowed
  `^0.7.7` to float while react pinned `0.7.7` exactly, so a future 0.7.x
  publish would have installed two copies with instances crossing the boundary.

Bundle impact: 340 B gzipped, down from 1128 B for the Store-only tanstack
bundle.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Aug 11, 2026 1:31pm
blocknote-website Ready Ready Preview Aug 11, 2026 1:31pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 10f367cf-47a7-4945-a191-5f68d8f375e0

📥 Commits

Reviewing files that changed from the base of the PR and between 489af88 and 2ee606c.

📒 Files selected for processing (3)
  • docs/content/docs/getting-started/vanilla-js.mdx
  • packages/core/src/util/Store.test.ts
  • packages/core/src/util/Store.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/content/docs/getting-started/vanilla-js.mdx
  • packages/core/src/util/Store.test.ts
  • packages/core/src/util/Store.ts

📝 Walkthrough

Walkthrough

The PR adds local core and React store implementations. Core and React integrations now use them instead of TanStack Store packages. The store API is publicly exported and documented.

Changes

Local store migration

Layer / File(s) Summary
Core Store implementation
packages/core/src/util/Store.ts, packages/core/src/util/Store.test.ts
Adds typed state updates, subscriptions, onUpdate, previous-state reporting, listener notifications, and iterative handling of re-entrant updates. Tests cover these behaviors.
Core Store integration
packages/core/src/editor/BlockNoteExtension.ts, packages/core/src/user/UserStore.ts, packages/core/src/comments/extension.ts, packages/core/src/index.ts, packages/core/package.json, docs/content/docs/getting-started/vanilla-js.mdx
Core imports the local Store, exports it publicly, removes the runtime dependency, updates comment selection handling, and documents the store API.
React Store binding
packages/react/src/hooks/useStore.ts, packages/react/src/hooks/useExtension.ts, packages/react/src/components/Comments/useCommentUsers.ts, packages/react/src/components/Versioning/useVersionUsers.ts, packages/react/src/index.ts, packages/react/package.json
Adds the local useStore hook and shallow comparator, updates React consumers, exports the hook, and removes the React Store dependency.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReactComponent
  participant useStore
  participant Store
  ReactComponent->>useStore: select store state
  useStore->>Store: subscribe(listener)
  Store-->>useStore: notify state and previous state
  useStore->>useStore: compare selection with shallow
  useStore-->>ReactComponent: return selected state
Loading

Poem

A rabbit checks the Store at dawn,
New state arrives, old state is gone.
Listeners hop in ordered lines,
Shallow checks keep values aligned.
Local hooks now guide the way.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: replacing TanStack Store dependencies with a first-party Store implementation.
Description check ✅ Passed The description covers all template sections, explains the rationale and impact, lists testing, and documents the known pre-existing e2e failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tanstack-store-upgrade-eval

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2956

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2956

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2956

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2956

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2956

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2956

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2956

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2956

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2956

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2956

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2956

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2956

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2956

commit: 2ee606c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/getting-started/vanilla-js.mdx`:
- Line 47: Update the Store API description in the UI element extensions
documentation to avoid limiting it to three members: change the wording to
“these primary members” while preserving the existing descriptions of state,
setState, and subscribe.

In `@packages/core/src/util/Store.ts`:
- Around line 74-76: Update the Store flush flow around flush and the onUpdate
callback so the flush transaction marks isFlushing before invoking onUpdate,
preventing nested setState calls from flushing immediately. Queue the store’s
pending update before draining notifications, and add coverage where onUpdate
performs a nested setState to verify subscribers receive the final state only
once.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a3f93509-ffdf-4287-9ddb-d9da6914a1e2

📥 Commits

Reviewing files that changed from the base of the PR and between dee3401 and 489af88.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • docs/content/docs/getting-started/vanilla-js.mdx
  • packages/core/package.json
  • packages/core/src/comments/extension.ts
  • packages/core/src/editor/BlockNoteExtension.ts
  • packages/core/src/index.ts
  • packages/core/src/user/UserStore.ts
  • packages/core/src/util/Store.test.ts
  • packages/core/src/util/Store.ts
  • packages/react/package.json
  • packages/react/src/components/Comments/useCommentUsers.ts
  • packages/react/src/components/Versioning/useVersionUsers.ts
  • packages/react/src/hooks/useExtension.ts
  • packages/react/src/hooks/useStore.ts
  • packages/react/src/index.ts
💤 Files with no reviewable changes (2)
  • packages/react/package.json
  • packages/core/package.json

Comment thread docs/content/docs/getting-started/vanilla-js.mdx Outdated
Comment thread packages/core/src/util/Store.ts Outdated
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-11 13:50 UTC

`onUpdate` ran before `flush()`, so a nested `setState` made from inside it saw
`isFlushing === false` and drained immediately; the outer `flush()` then drained
a second time. Subscribers were notified twice with identical values.

Move `onUpdate` inside the flush transaction: the store is queued and the flush
marked in progress before the callback fires, so a nested write is coalesced
into the in-progress drain. Subscribers now see the settled state exactly once.
Ordering is unchanged — `onUpdate` still runs after the write and before
listeners.

Also clear the pending queue when the outermost flush unwinds, so a throwing
callback can't strand a queued store and have it notify during an unrelated
store's next flush.

Docs: describe the store's `state`/`setState`/`subscribe` as its primary
members rather than an exhaustive set of three.
@nperez0111
nperez0111 merged commit bea469e into main Aug 11, 2026
28 checks passed
@nperez0111
nperez0111 deleted the feat/tanstack-store-upgrade-eval branch August 11, 2026 13:50
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.

1 participant