Tiptap v3のRelease Issuesでeditor.isActive
が動かない問題が報告されているのでそれについて軽く調べてみます。
pnpm create vite
で作ったReact+TypeScriptプロジェクトにTiptapをインストールします。デフォルトではTiptap@^2.12.0がインストールされます。
pnpm create vite
pnpm add @tiptap/pm @tiptap/react @tiptap/starter-kit
とりあえずv2.12.0
で動くことを確認します。公式のBold Extensionのサンプルなどを参考にエディターを作ります。
Tiptap.tsx
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
const extensions = [StarterKit];
const content = `
This isn’t bold.
This is bold.
And this.
This as well.
Oh, and this!
Cool, isn’t it!?
Up to font weight 999!!!
`;
export const Tiptap = () => {
const editor = useEditor({
extensions,
content,
});
if (!editor) return null;
return (
>
div className="control-group">
div className="button-group">
button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "is-active" : ""}
>
Toggle bold
/button>
button
onClick={() => editor.chain().focus().setBold().run()}
disabled={editor.isActive("bold")}
>
Set bold
/button>
button
onClick={() => editor.chain().focus().unsetBold().run()}
disabled={!editor.isActive("bold")}
>
Unset bold
/button>
/div>
/div>
EditorContent editor={editor} />
/>
);
};
pnpm dev
で開発サーバーを立ちげて、isActiveが動作することを確認できました。
現時点(2025/5/18)でv3の最新であるv3.0.0-next.8
をインストールします。package.jsonのTiptapのバージョンを3.0.0-next.8
に書き換えてpnpm i
します。
これでisActive動かないことを確認できました。
以前書いた記事でも触れましたが、v2.12.0でも上記のサンプルは動かなくなるパターンがあります。
一度Tiptapのバージョンをv2.12.0
に戻して以下のようにコンポーネントを書き換えます。editorインスタンスを受け取るコンポーネントをmemo化しています。
少し余談ですがTiptapのドキュメントでもuseEditorによる再レンダリングの影響を抑えるために、エディター以外のコンポーネントを分割してeditorインスタンスをpropsで渡して参照するパターンが紹介されています。
Tiptap.tsx
import { memo } from "react";
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
const extensions = [StarterKit];
const content = `
This isn’t bold.
This is bold.
And this.
This as well.
Oh, and this!
Cool, isn’t it!?
Up to font weight 999!!!
`;
export const Tiptap = () => {
const editor = useEditor({
extensions,
content,
});
if (!editor) return null;
return (
>
OperationsMemo editor={editor} />
EditorContent editor={editor} />
/>
);
};
const OperationsMemo = memo(Operations);
function Operations({ editor }: { editor: Editor }) {
return (
div className="control-group">
div className="button-group">
button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "is-active" : ""}
>
Toggle bold
/button>
button
onClick={() => editor.chain().focus().setBold().run()}
disabled={editor.isActive("bold")}
>
Set bold
/button>
button
onClick={() => editor.chain().focus().unsetBold().run()}
disabled={!editor.isActive("bold")}
>
Unset bold
/button>
/div>
/div>
);
}
これでv2.12.0
でも動かなくなります。理由としてはeditorインスタンスの比較結果がレンダリング間で変わらないためmemo化により再レンダリングしなくなったためです。memo化していないときに動いていたのはコンポーネントの再レンダリングによりeditor.isActive
関数が実行されていたためです。
そしてコンポーネントの再レンダリングに依存するのは得策ではないです。
これを動かすにはやはり「エディター上のイベントを監視できるonUpdateオプションなどでsetStateしてReactコンポーネントを更新する」のが良いです。
今回はeditor.isActive('bold')
の結果をエディター上のトランザクションが走るたびにsetStateします。コンテンツの更新だけでなくカーソルの移動なども検知したいため、監視するエディター上のイベントはonTransaction
を使います。
Tiptap.tsx
export const Tiptap = () => {
const [isActiveBold, setIsActiveBold] = useState(false);
const editor = useEditor({
extensions,
content,
onTransaction: ({ editor }) => {
setIsActiveBold(editor.isActive("bold"));
},
});
if (!editor) return null;
return (
>
OperationsMemo editor={editor} isActiveBold={isActiveBold} />
EditorContent editor={editor} />
/>
);
};
これでカーソル位置が変わったときにカーソル位置がBoldかどうかという状態をReactのStateに紐付けることができました。このStateはトランザクションの度にsetStateされるのでそれに反応してコンポーネントのUIも更新されます。結果memo化していても動作するようになりました。
この方法はv3.0.0-next.8
でも動作します。
ということでTiptapとReactの連携はTiptapのイベントをフックしてsetStateすれば良いというのが個人的な考えです。公式ではuseEditorStateを使う方法も紹介されているのでこちらを使うのも良いかもしれません。
とはいえ同じサンプルコードでv2からv3で動かないというバグがなぜ発生するのかは気になります。ということで調査してみたところ原因は以下の変更でした。
エディターのtransactionの度に再レンダリングしなくなった変更です。shouldRerenderOnTransaction
オプションはv2.5.0から追加されたオプションですが、これがv3ではデフォルトでfalseになりtransaction毎に再レンダリングされなくなりました。結果、前述したmemo化の例と同じように再レンダリングによるeditor.isActive
の実行がなくなり、UIにeditor.isActive
の状態が反映されなくなりました。
shouldRerenderOnTransaction
のdefaltがfalseになったことはパフォーマンス面の強化になっていると思います。とはいえ、エディターの再レンダリングに依存している公式のサンプルなど動かなくなるパターンも存在するため、shouldRerenderOnTransaction
オプションの位置付けをTiptapチームがどうしていくのか動向を追いたいと思いました。
Views: 0