定義
日本語や韓国語を打っている途中、まだ変換が確定していない状態(韓国語なら「ㅇ ㅏ ㄴ」のように組み合わせが終わっていない状態)で画面が一度再描画されると、文字が崩れたり二重に打たれたりする経験をしたことがあるだろう。この概念は、自作のエディターでその崩れを防ぎたいときに使う。
IME(Input Method Editor、複数のキー入力を組み合わせて一文字を作る入力機)の変換(composition)は、韓国語・日本語・中国語のように複数のキー入力が一つの文字として確定するまで、ブラウザ/OS が保持する native composition session(文字が確定する前の「一時的な入力セッション」)だ。contenteditable(ユーザーが直接文字を編集できるようにした HTML 要素)ベースのリッチテキストエディターで、変換中に DOM 入れ替え・React re-render・serialize commit(エディターの内容を文字列/データとして保存する過程)が割り込むとセッションが切れ、変換中の文字が重複・消失・早期確定する。
なぜ必要か
ウェブエディターは「キー入力 → state 更新 → DOM 反映」ループが速い。IME はそのループの途中で「まだ確定していない文字列」を保持しなければならない。変換中に親が guest value を prop で上書きしたり、blur/unmount の経路が commit を呼ぶと、ブラウザの underlines・変換バッファがリセットされる。カスタムの compositionstart/end イベントを新しく設計する前に、使っているエディターがcomposing フラグをすでに提供しているか確認するほうが、費用対効果が大きい。
動作原理
- ユーザーが IME で入力を始めると、ブラウザが composition session を開く(underline 表示)。
beforeinput/inputイベントは、変換中・変換後の段階で異なって来る。- エディターは
view.composing(ProseMirror)またはnativeEvent.isComposingのような信号で「今は flush 禁止」を判断する。 - 変換が
compositionendで確定したら、そのとき commit/serialize が安全だ。 - React guest パターンでは、mount-once initial content で親の re-render が composing テキストを clobber しないようにする。
| 段階 | 危険な動作 | 安全なパターン |
|---|---|---|
| 変換中 | blur commit, plugin menu open | composing gate |
| 変換中 | parent prop で value replace | mount-once guest |
| 検証 | jsdom keyboard only | CDP imeSetComposition |
実務での適用
Playwright(Chromium)では DevTools Protocol の Input.imeSetComposition で変換段階を順次再現し、Input.insertText で確定する。jsdom・happy-dom は composition のライフサイクルを忠実には模さないので、IME 回帰の唯一の根拠になりにくい。
トレードオフ
- composing defer は**変換中の構造変更(split/merge)**を遅延させうる — たいてい数 ms〜数百 ms で UX 上許容される。
- mount-once guest は「親が強制的に content reset」パターンと衝突する — reset が必要なら keyed remount で明示的に再開する。
- Safari/WebKit と Chromium の composition タイミングの差は環境ごとに異なりうるので、クロスブラウザ E2E が安全だ。
使ってはいけない場合
- 単一言語(ASCII only)の内部ツールで IME をまったく対応しないと明示的にスコープを制限した場合(それでもモバイル OS のキーボードは IME 経路を通りうる)。
- composition イベントだけ追加して commit ゲートはそのままにする場合 — イベントハンドラだけでは flush 経路全体を塞ぎにくい。
よくある間違い
- 「ユニットテストが green だから IME OK」 — jsdom は composition・座標 API が不完全だ。
- slash menu・mention plugin が
view.updateごとに setState — 変換中でも親が re-render。 - blur/unmount/keyboard delegate ごとに commit を複製 — composing チェックの漏れ。
関連概念
- focused-guest-editor — guest commit の境界・mount-once
- contenteditable-keyboard-history-delegation — focus 中のキー・履歴の相互作用