React でエディターの状態を管理する
現在の文書の持ち主は 1 つにしてください。アプリケーションのほかの部分が変更のたびにそれを必要とする場合は制御された状態(controlled)を、主に保存時にコンテンツを読み取ればよい場合は非制御(uncontrolled)のエディターを選びます。これらの例では、npm の React バインディングを HTML モードで使います。
制御された状態:ローカルの状態を最新に保つ
"use client";
import { useState } from "react";
import { RichTextEditor } from "@richscripts/richtexteditor/react";
export default function Composer() {
const [html, setHtml] = useState("<p>Start writing...</p>");
return <RichTextEditor
value={html}
onChange={(next) => {
if (typeof next === "string") setHtml(next);
}}
/>;
}エディターが出力した値を、ローカルの編集中の値として保持してください。古い値をわざわざエディターに戻すのではなく、ネットワークへの保存は別にデバウンスします。保存のたびに、文書全体をサーバーで正規化したコピーで置き換えることは避けてください。
バインディングは、自分が出力した値と外部からの置き換えを区別します。ただし、アプリケーションが行うあらゆる変換について、選択範囲を安全に保てるわけではありません。HTML を整形し直して異なるコンテンツを返す onChange ハンドラーは、誰かが入力している最中にコンテンツを置き換えるようエディターに求めていることになります。
非制御:保存時に現在の HTML を読み取る
"use client";
import { useRef, useState } from "react";
import { RichTextEditor } from "@richscripts/richtexteditor/react";
import type { RichTextEditorReactHandle } from "@richscripts/richtexteditor/react";
export default function Draft({ initialHtml, onSave }: {
initialHtml: string;
onSave: (html: string) => void;
}) {
const editorRef = useRef<RichTextEditorReactHandle>(null);
const [ready, setReady] = useState(false);
return <>
<RichTextEditor ref={editorRef} defaultValue={initialHtml}
onReady={() => setReady(true)} />
<button type="button" disabled={!ready} onClick={() => {
if (editorRef.current?.getEditor()) {
onSave(editorRef.current.getHTMLCode());
}
}}>Save</button>
</>;
}defaultValue は文書の初期値を与えるものであり、マウント済みのエディターにその後の変更を送り込む方法ではありません。ref は getHTMLCode() を公開しています。準備完了のチェックにより、早すぎる保存で、まだ初期化されていないエディターが空の文書として扱われるのを防ぎます。
この例は HTML をアプリケーションのコールバックに渡すだけで、保存、アップロードの完了、保存エラーからの回復は実装していません。画像を有効にしている場合は、最終的な HTML を読み取る前にアップロードと保存の手順を使ってください。送信されたコンテンツは、サーバーで検証してサニタイズしてください。
文書の切り替えとサーバーのレスポンスを安全に扱う
- 文書の識別子は安定したものを使ってください。現在の HTML や乱数に基づく
keyでは、編集中にエディターが再マウントされてしまいます。 - 意図的に文書の ID を key にすれば、別の文書のために新しいエディターを始められます。切り替える前に、変更のある下書きを確認するか保存してください。再マウントすると、選択範囲や元に戻す履歴などのインスタンスの状態は破棄されます。
- 文書を非同期に読み込む場合は、すでに選択されていない文書へのレスポンスは無視するか中止してください。
- 保存するときは文書のリビジョンを追跡してください。遅れて届いたレスポンスは、自分のリビジョンの保存状態だけを更新すべきで、新しい下書きを上書きしてはいけません。ほかのユーザーによる競合する保存を検出するには、サーバー側でのリビジョンのチェックが必要です。
- セッションの途中で、制御されたモードと非制御のモードを切り替えないでください。パターンは文書を開くときに選びます。
マウントと Strict Mode
同じホストに 2 つ目のエディターを作成するのではなく、提供されている React のラッパーを使ってください。独自のラッパーを書く場合は、ホストが存在するようになってからエディターを作成し、クリーンアップでは自分が所有するリスナーを外して、所有するインスタンスを破棄してください。アンマウント後に完了した非同期の初期化は無視します。
React の Strict Mode は、開発時にセットアップとクリーンアップのサイクルを余分に実行することがあります。ライフサイクルの動作を確認する間は、有効にしたままにしてください。Next.js では、クライアントコンポーネントへの組み込みガイドに従ってください。保存したコンテンツを表示することと、対話型のエディターをマウントすることは、別々の関心事です。
トラブルシューティングのチェックリスト
- カーソルが飛ぶ:変化する key、作り直されるコンポーネント、繰り返される
setHTMLCode()の呼び出し、変換された制御値を確認してください。 - 入力した内容が消える:遅延したコールバック、保存のレスポンス、文書の読み込みから来る古い値を確認してください。
- 保存に最後の編集が含まれない:遅延した変更通知によってすでに React の状態が更新されていると想定せず、送信時にエディターの ref を読み取ってください。
- ツールバーが重複する:ルートの変更やモーダルの再表示を含め、初期化とクリーンアップが重複していないかを確認してください。
- 大きな下書きで動作が遅い:HTML のシリアライズ、プレビューの描画、ネットワークへの保存を個別にプロファイリングしてください。
- IME の入力が壊れる:変換中にコンテンツを書き換えずに中国語や日本語の変換入力をテストし、テキストが確定した後の元に戻す操作を確認してください。
次は、カーソルが飛ぶ問題を順を追って診断する、React バインディングをインストールする、またはフォームの送信と保存と読み込みを試してください。