refactor: vendor @tanstack/store as a first-party Store - #2956
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe 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. ChangesLocal store migration
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
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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
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. Comment |
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/mantine
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
docs/content/docs/getting-started/vanilla-js.mdxpackages/core/package.jsonpackages/core/src/comments/extension.tspackages/core/src/editor/BlockNoteExtension.tspackages/core/src/index.tspackages/core/src/user/UserStore.tspackages/core/src/util/Store.test.tspackages/core/src/util/Store.tspackages/react/package.jsonpackages/react/src/components/Comments/useCommentUsers.tspackages/react/src/components/Versioning/useVersionUsers.tspackages/react/src/hooks/useExtension.tspackages/react/src/hooks/useStore.tspackages/react/src/index.ts
💤 Files with no reviewable changes (2)
- packages/react/package.json
- packages/core/package.json
|
`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.
Summary
Replaces the
@tanstack/storeand@tanstack/react-storedependencies with a ~50-line first-partyStorein@blocknote/coreand a matchinguseStorehook in@blocknote/react.Rationale
We only ever used
state,prevState,setState,subscribeand theonUpdateoption — never theDerived/Effectreactive graph that makes up the rest of the library, which also doesn't tree-shake becausesetStatereaches into the scheduler that importsDerived. SinceStoreis 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
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.packages/react/src/hooks/useStore.ts, wrapping theuse-sync-external-storeshim@blocknote/reactalready depends on, and preserving theshallowdefault comparator thatuseCommentUsers/useVersionUsersrely on.onUpdatenow receives the new and previous state, so callers no longer close over the store to readprevState.TUpdatertype parameter, which existed only to serve the unusedupdateFnoption.Impact
No behavioural change and no public API break:
subscribestill returns an unsubscribe function,setStatestill accepts a value or an updater, and the listener payload is unchanged. Emitted declarations no longer reference@tanstack/storeat all (previously 19+ references) and simplify fromStore<T, (cb: T) => T>toStore<T>. This also closes a version-skew hazard where core allowed^0.7.7to float while react pinned0.7.7exactly, so a future 0.7.x publish would have installed two copies withStoreinstances crossing the package boundary. Bundle cost drops from 1128 B to 340 B gzipped.Testing
New
packages/core/src/util/Store.test.tscovers value- and function-formsetState,prevState,onUpdateordering 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
Additional Notes
keyboardhandlers.test.tsx(2 snapshot mismatches) andmulticolumnDrop.test.tsx(aRangeError: Position 30 outside of fragmentinmultiColumnHandleDropPlugin.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 onalien-signalsdirectly — it is past 1.0, has zero runtime dependencies, and is the same engine@tanstack/storeitself rewrote onto — rather than re-adopting a 0.x store wrapper.Summary by CodeRabbit
New Features
useStorehook through the public packages.Bug Fixes
Documentation