diff --git a/packages/core/package.json b/packages/core/package.json index f20292ad57..46aa029b13 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -92,7 +92,7 @@ "@emoji-mart/data": "^1.2.1", "@handlewithcare/prosemirror-inputrules": "^0.1.4", "@shikijs/types": "^4", - "@tanstack/store": "^0.7.7", + "@tanstack/store": "^0.10.0", "@tiptap/core": "^3.13.0", "@tiptap/extension-bold": "^3.13.0", "@tiptap/extension-code": "^3.13.0", diff --git a/packages/core/src/comments/extension.ts b/packages/core/src/comments/extension.ts index 4e8e566cef..13a47cf825 100644 --- a/packages/core/src/comments/extension.ts +++ b/packages/core/src/comments/extension.ts @@ -91,24 +91,11 @@ export const CommentsExtension = createExtension( const markType = CommentMark.name; const userStore = new UserStore(resolveUsers); - const store = createStore( - { - pendingComment: false, - selectedThreadId: undefined as string | undefined, - threadPositions: new Map(), - }, - { - onUpdate() { - // If the selected thread id changed, we need to update the decorations - if ( - store.state.selectedThreadId !== store.prevState.selectedThreadId - ) { - // So, we issue a transaction to update the decorations - editor.transact((tr) => tr.setMeta(PLUGIN_KEY, true)); - } - }, - }, - ); + const store = createStore({ + pendingComment: false, + selectedThreadId: undefined as string | undefined, + threadPositions: new Map(), + }); const updateMarksFromThreads = (threads: Map) => { editor.transact((tr) => { @@ -261,6 +248,16 @@ export const CommentsExtension = createExtension( const unsubscribe = threadStore.subscribe(updateMarksFromThreads); updateMarksFromThreads(threadStore.getThreads()); + let prevSelectedThreadId = store.state.selectedThreadId; + const storeSubscription = store.subscribe(() => { + // If the selected thread id changed, we need to update the decorations + if (store.state.selectedThreadId !== prevSelectedThreadId) { + prevSelectedThreadId = store.state.selectedThreadId; + // So, we issue a transaction to update the decorations + editor.transact((tr) => tr.setMeta(PLUGIN_KEY, true)); + } + }); + const unsubscribeOnSelectionChange = editor.onSelectionChange(() => { if (store.state.pendingComment) { store.setState((prev) => ({ @@ -272,6 +269,7 @@ export const CommentsExtension = createExtension( return () => { unsubscribe(); + storeSubscription.unsubscribe(); unsubscribeOnSelectionChange(); }; }, diff --git a/packages/core/src/editor/BlockNoteExtension.ts b/packages/core/src/editor/BlockNoteExtension.ts index 7346333990..0a82f548a9 100644 --- a/packages/core/src/editor/BlockNoteExtension.ts +++ b/packages/core/src/editor/BlockNoteExtension.ts @@ -1,4 +1,4 @@ -import { Store, StoreOptions } from "@tanstack/store"; +import { Store } from "@tanstack/store"; import { type AnyExtension } from "@tiptap/core"; import type { Plugin as ProsemirrorPlugin } from "prosemirror-state"; import type { PartialBlockNoDefaults } from "../schema/index.js"; @@ -233,9 +233,6 @@ export function createExtension< } as any; } -export function createStore( - initialState: T, - options?: StoreOptions, -): Store { - return new Store(initialState, options); +export function createStore(initialState: T): Store { + return new Store(initialState); } diff --git a/packages/core/src/extensions/Collaboration/ForkYDoc.ts b/packages/core/src/extensions/Collaboration/ForkYDoc.ts index 84c714f1d3..31315bc06d 100644 --- a/packages/core/src/extensions/Collaboration/ForkYDoc.ts +++ b/packages/core/src/extensions/Collaboration/ForkYDoc.ts @@ -106,7 +106,7 @@ export const ForkYDocExtension = createExtension( ]); // Tell the store that the editor is now forked - store.setState({ isForked: true }); + store.setState(() => ({ isForked: true })); }, /** @@ -146,7 +146,7 @@ export const ForkYDocExtension = createExtension( // Reset the forked state forkedState = undefined; // Tell the store that the editor is no longer forked - store.setState({ isForked: false }); + store.setState(() => ({ isForked: false })); }, } as const; }, diff --git a/packages/core/src/extensions/FilePanel/FilePanel.ts b/packages/core/src/extensions/FilePanel/FilePanel.ts index ebf4d96991..0ed8e56622 100644 --- a/packages/core/src/extensions/FilePanel/FilePanel.ts +++ b/packages/core/src/extensions/FilePanel/FilePanel.ts @@ -7,7 +7,7 @@ export const FilePanelExtension = createExtension(({ editor }) => { const store = createStore(undefined); function closeMenu() { - store.setState(undefined); + store.setState(() => undefined); } return { @@ -35,7 +35,7 @@ export const FilePanelExtension = createExtension(({ editor }) => { }, closeMenu, showMenu(blockId: string) { - store.setState(blockId); + store.setState(() => blockId); }, } as const; }); diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts index 021c20ea3d..b1202879b3 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts @@ -67,14 +67,14 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { return; } // re-evaluate whether the toolbar should be shown - store.setState(shouldShow()); + store.setState(() => shouldShow()); }); const unsubscribeOnSelectionChange = editor.onSelectionChange(() => { if (preventShowWhileMouseDown) { return; } // re-evaluate whether the toolbar should be shown - store.setState(shouldShow()); + store.setState(() => shouldShow()); }); // To mimic Notion's behavior, we listen to the mouse down event to set the `preventShowWhileMouseDown` flag @@ -82,7 +82,7 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { "pointerdown", () => { preventShowWhileMouseDown = true; - store.setState(false); + store.setState(() => false); }, { signal }, ); @@ -93,7 +93,7 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { preventShowWhileMouseDown = false; // We only want to re-show the toolbar if the mouse made the selection if (editor.isFocused()) { - store.setState(shouldShow()); + store.setState(() => shouldShow()); } }, { signal, capture: true }, diff --git a/packages/core/src/extensions/ShowSelection/ShowSelection.ts b/packages/core/src/extensions/ShowSelection/ShowSelection.ts index c4698fbde7..22e38aaf46 100644 --- a/packages/core/src/extensions/ShowSelection/ShowSelection.ts +++ b/packages/core/src/extensions/ShowSelection/ShowSelection.ts @@ -13,14 +13,7 @@ const PLUGIN_KEY = new PluginKey(`blocknote-show-selection`); * text editor is not focused. */ export const ShowSelectionExtension = createExtension(({ editor }) => { - const store = createStore( - { enabledSet: new Set() }, - { - onUpdate() { - editor.transact((tr) => tr.setMeta(PLUGIN_KEY, {})); - }, - }, - ); + const store = createStore({ enabledSet: new Set() }); return { key: "showSelection", store, @@ -41,6 +34,14 @@ export const ShowSelectionExtension = createExtension(({ editor }) => { }, }), ], + mount() { + const subscription = store.subscribe(() => { + editor.transact((tr) => tr.setMeta(PLUGIN_KEY, {})); + }); + return () => { + subscription.unsubscribe(); + }; + }, /** * Show or hide the selection decoration * @@ -51,11 +52,11 @@ export const ShowSelectionExtension = createExtension(({ editor }) => { * (e.g.: CreateLinkButton and AIExtension) */ showSelection(shouldShow: boolean, key: string) { - store.setState({ + store.setState((prev) => ({ enabledSet: shouldShow - ? new Set([...store.state.enabledSet, key]) - : new Set([...store.state.enabledSet].filter((k) => k !== key)), - }); + ? new Set([...prev.enabledSet, key]) + : new Set([...prev.enabledSet].filter((k) => k !== key)), + })); }, } as const; }); diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index 635929a756..e43bbf5256 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -129,8 +129,7 @@ export class SideMenuView< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema, -> implements PluginView -{ +> implements PluginView { public state?: SideMenuState; public readonly emitUpdate: (state: SideMenuState) => void; @@ -725,7 +724,7 @@ export const SideMenuExtension = createExtension(({ editor }) => { view = new SideMenuView(editor, editorView, (state) => { // TODO: Without spreading the state, in some cases like toggling // `show`, this doesn't trigger an update. - store.setState({ ...state }); + store.setState(() => ({ ...state })); }); return view; }, diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts index 4809607e6e..b60cfb172b 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts @@ -228,7 +228,7 @@ export const SuggestionMenu = createExtension(({ editor }) => { view = new SuggestionMenuView( editor, (triggerCharacter, state) => { - store.setState({ ...state, triggerCharacter }); + store.setState(() => ({ ...state, triggerCharacter })); }, v, ); diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts index 7f3cf774b5..59137e94e4 100644 --- a/packages/core/src/extensions/TableHandles/TableHandles.ts +++ b/packages/core/src/extensions/TableHandles/TableHandles.ts @@ -625,7 +625,7 @@ export const TableHandlesExtension = createExtension(({ editor }) => { key: tableHandlesPluginKey, view: (editorView) => { view = new TableHandlesView(editor as any, editorView, (state) => { - store.setState( + store.setState(() => state.block ? { ...state, diff --git a/packages/react/package.json b/packages/react/package.json index 3d006e0c51..f578ffabd9 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -62,7 +62,7 @@ "@emoji-mart/data": "^1.2.1", "@floating-ui/react": "^0.27.18", "@floating-ui/utils": "^0.2.10", - "@tanstack/react-store": "0.7.7", + "@tanstack/react-store": "0.10.0", "@tiptap/core": "^3.13.0", "@tiptap/pm": "^3.13.0", "@tiptap/react": "^3.13.0", diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx index 470a50dcba..c1f6dceba2 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx @@ -20,7 +20,7 @@ export const AddCommentButtonInner = () => { const onClick = useCallback(() => { comments.startPendingComment(); - store.setState(false); + store.setState(() => false); }, [comments, store]); return ( diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx index 925c6b7d12..9a7a8e6f8a 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx @@ -138,7 +138,9 @@ export const CreateLinkButton = () => { text={state.text} range={state.range} showTextField={false} - setToolbarOpen={(open) => formattingToolbar.store.setState(open)} + setToolbarOpen={(open) => + formattingToolbar.store.setState(() => open) + } /> diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx index 20184e626f..21a9a2ab4b 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx @@ -85,7 +85,7 @@ export const FormattingToolbarController = (props: { // Needed as hooks like `useDismiss` call `onOpenChange` to change the // open state. onOpenChange: (open, _event, reason) => { - formattingToolbar.store.setState(open); + formattingToolbar.store.setState(() => open); if (reason === "escape-key") { editor.focus(); diff --git a/packages/react/src/hooks/useExtension.ts b/packages/react/src/hooks/useExtension.ts index d6e4fcd526..22884f509c 100644 --- a/packages/react/src/hooks/useExtension.ts +++ b/packages/react/src/hooks/useExtension.ts @@ -59,5 +59,8 @@ export function useExtensionState< if (!store) { throw new Error("Store not found on plugin", { cause: { plugin } }); } - return useStore, TSelected>(store, ctx?.selector as any); + return useStore( + store as any, + (ctx?.selector ?? ((s: any) => s)) as any, + ) as TSelected; } diff --git a/packages/xl-ai/src/AIExtension.ts b/packages/xl-ai/src/AIExtension.ts index cc6f3c2a20..acd467d74a 100644 --- a/packages/xl-ai/src/AIExtension.ts +++ b/packages/xl-ai/src/AIExtension.ts @@ -147,12 +147,12 @@ export const AIExtension = createExtension( .getExtension(ShowSelectionExtension) ?.showSelection(true, "aiMenu"); editor.isEditable = false; - store.setState({ + store.setState(() => ({ aiMenuState: { blockId: blockID, status: "user-input", }, - }); + })); // Scrolls to the block when the menu opens. const blockElement = editor.domElement?.querySelector( @@ -165,9 +165,9 @@ export const AIExtension = createExtension( * Close the AI menu */ closeAIMenu() { - store.setState({ + store.setState(() => ({ aiMenuState: "closed", - }); + })); chatSession = undefined; editor .getExtension(ShowSelectionExtension) @@ -350,20 +350,20 @@ export const AIExtension = createExtension( if (status.status !== "error") { throw new UnreachableCaseError(status.status); } - this.store.setState({ + this.store.setState(() => ({ aiMenuState: { status: status.status, error: status.error, blockId: aiMenuState.blockId, }, - }); + })); } else { - this.store.setState({ + this.store.setState(() => ({ aiMenuState: { status: status, blockId: aiMenuState.blockId, }, - }); + })); } }, @@ -441,12 +441,12 @@ export const AIExtension = createExtension( } // NOTE: does this setState with an anon object trigger unnecessary re-renders? - store.setState({ + store.setState(() => ({ aiMenuState: { blockId, status: "ai-writing", }, - }); + })); if (autoScroll) { const blockElement = editor.prosemirrorView.domAtPos( diff --git a/packages/xl-ai/src/components/FormattingToolbar/AIToolbarButton.tsx b/packages/xl-ai/src/components/FormattingToolbar/AIToolbarButton.tsx index c2ce5b5745..52ad783d8d 100644 --- a/packages/xl-ai/src/components/FormattingToolbar/AIToolbarButton.tsx +++ b/packages/xl-ai/src/components/FormattingToolbar/AIToolbarButton.tsx @@ -31,7 +31,7 @@ export const AIToolbarButton = () => { const position = selection.blocks[selection.blocks.length - 1].id; ai.openAIMenuAtBlock(position); - formattingToolbar.store.setState(false); + formattingToolbar.store.setState(() => false); }; if (!editor.isEditable) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2cd8b1127..579a21e437 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4676,8 +4676,8 @@ importers: specifier: ^4 version: 4.0.2 '@tanstack/store': - specifier: ^0.7.7 - version: 0.7.7 + specifier: ^0.10.0 + version: 0.10.0 '@tiptap/core': specifier: ^3.13.0 version: 3.22.3(@tiptap/pm@3.22.3) @@ -4919,8 +4919,8 @@ importers: specifier: ^0.2.10 version: 0.2.11 '@tanstack/react-store': - specifier: 0.7.7 - version: 0.7.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: 0.10.0 + version: 0.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tiptap/core': specifier: ^3.13.0 version: 3.22.3(@tiptap/pm@3.22.3) @@ -10377,8 +10377,8 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tanstack/react-store@0.7.7': - resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + '@tanstack/react-store@0.10.0': + resolution: {integrity: sha512-S1rdnL5OtGLpUBlM9hrQ0heGZFBIzjIwhqrboFWq4pAAufpOuf2eL/dqyupXO9OCblwxBhuB2n43ZcDNqrtoDw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -10390,8 +10390,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/store@0.7.7': - resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/store@0.10.0': + resolution: {integrity: sha512-KXAPlXun5J9DjO5LYo+EVjVHGdHfOl5YWE9TVPh4MeHl6gIngIV0fC5rE22JTRZa9KCMiKidh5FG6ufE2usr7Q==} '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} @@ -21748,9 +21748,9 @@ snapshots: tailwindcss: 4.2.2 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@tanstack/react-store@0.7.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-store@0.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/store': 0.7.7 + '@tanstack/store': 0.10.0 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) use-sync-external-store: 1.6.0(react@19.2.5) @@ -21761,7 +21761,7 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@tanstack/store@0.7.7': {} + '@tanstack/store@0.10.0': {} '@tanstack/table-core@8.21.3': {}