From 996fcf49fa5648f13da6e99be2c053cd535b4ed2 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Fri, 3 Jul 2026 11:42:34 +0700 Subject: [PATCH] fix(markdown-editor): drive toolbar state via useEditorState (M4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tiptap v3's useEditor does not re-render the component on every transaction, so the toolbar's 16 editor.isActive()/can() reads went stale on selection-only cursor moves — click into a bold word and the Bold button stayed unlit until the next doc edit. Read all button states through a single useEditorState selector, which re-runs per transaction and re-renders only when a state flips. Fixes the staleness and drops the incidental full re-render. Idiomatic tiptap v3 pattern. Co-Authored-By: Claude Opus 4.8 --- .../markdown-editor/markdown-editor.tsx | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor.tsx b/frontend/src/components/shared/markdown-editor/markdown-editor.tsx index 2851fb88..b0875534 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor.tsx +++ b/frontend/src/components/shared/markdown-editor/markdown-editor.tsx @@ -2,7 +2,7 @@ import type { Editor } from '@tiptap/react'; import { history } from '@tiptap/pm/history'; import { EditorState, TextSelection } from '@tiptap/pm/state'; -import { EditorContent, useEditor } from '@tiptap/react'; +import { EditorContent, useEditor, useEditorState } from '@tiptap/react'; import { Bold, Code, @@ -327,6 +327,31 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); }, [editor]); + // tiptap v3's useEditor does NOT re-render on every transaction, so reading editor.isActive/can() inline + // would leave the toolbar stale on selection-only moves (click into a bold word → Bold stays unlit). + // useEditorState re-runs this selector per transaction and re-renders only when a button's state flips. + const state = useEditorState({ + editor, + selector: ({ editor }) => ({ + canRedo: editor.can().redo(), + canUndo: editor.can().undo(), + isBlockquote: editor.isActive('blockquote'), + isBold: editor.isActive('bold'), + isBulletList: editor.isActive('bulletList'), + isCode: editor.isActive('code'), + isCodeBlock: editor.isActive('codeBlock'), + isH1: editor.isActive('heading', { level: 1 }), + isH2: editor.isActive('heading', { level: 2 }), + isH3: editor.isActive('heading', { level: 3 }), + isItalic: editor.isActive('italic'), + isLink: editor.isActive('link'), + isOrderedList: editor.isActive('orderedList'), + isStrike: editor.isActive('strike'), + isTable: editor.isActive('table'), + isTaskList: editor.isActive('taskList'), + }), + }); + return (
editor.chain().focus().toggleBold().run()} - pressed={editor.isActive('bold')} + pressed={state.isBold} size="sm" title="Bold (Ctrl+B)" > @@ -347,7 +372,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleItalic().run()} - pressed={editor.isActive('italic')} + pressed={state.isItalic} size="sm" title="Italic (Ctrl+I)" > @@ -356,7 +381,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleStrike().run()} - pressed={editor.isActive('strike')} + pressed={state.isStrike} size="sm" title="Strikethrough" > @@ -365,7 +390,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleCode().run()} - pressed={editor.isActive('code')} + pressed={state.isCode} size="sm" title="Inline code" > @@ -380,7 +405,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleHeading({ level: 1 }).run()} - pressed={editor.isActive('heading', { level: 1 })} + pressed={state.isH1} size="sm" title="Heading 1" > @@ -389,7 +414,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleHeading({ level: 2 }).run()} - pressed={editor.isActive('heading', { level: 2 })} + pressed={state.isH2} size="sm" title="Heading 2" > @@ -398,7 +423,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleHeading({ level: 3 }).run()} - pressed={editor.isActive('heading', { level: 3 })} + pressed={state.isH3} size="sm" title="Heading 3" > @@ -413,7 +438,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleBulletList().run()} - pressed={editor.isActive('bulletList')} + pressed={state.isBulletList} size="sm" title="Bullet list" > @@ -422,7 +447,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleOrderedList().run()} - pressed={editor.isActive('orderedList')} + pressed={state.isOrderedList} size="sm" title="Ordered list" > @@ -431,7 +456,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleTaskList().run()} - pressed={editor.isActive('taskList')} + pressed={state.isTaskList} size="sm" title="Task list" > @@ -446,7 +471,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleBlockquote().run()} - pressed={editor.isActive('blockquote')} + pressed={state.isBlockquote} size="sm" title="Blockquote" > @@ -455,7 +480,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().toggleCodeBlock().run()} - pressed={editor.isActive('codeBlock')} + pressed={state.isCodeBlock} size="sm" title="Code block" > @@ -464,7 +489,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) @@ -499,7 +524,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) onPressedChange={() => editor.chain().focus().insertTable({ cols: 3, rows: 3, withHeaderRow: true }).run() } - pressed={editor.isActive('table')} + pressed={state.isTable} size="sm" title="Insert table" > @@ -509,7 +534,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps)
editor.chain().focus().undo().run()} pressed={false} size="sm" @@ -519,7 +544,7 @@ function MarkdownEditorToolbar({ disabled, editor }: MarkdownEditorToolbarProps) editor.chain().focus().redo().run()} pressed={false} size="sm"