ReactでIME変換中に起きる2つの落とし穴——Enter送信とデバウンス自動検索

ReactでIME変換中に起きる2つの落とし穴——Enter送信とデバウンス自動検索

日本語入力(IME)変換中のEnterキーでフォームが誤送信される問題と、デバウンス自動検索がIME未確定文字で発火する問題の原因と修正方法を解説します。isComposingプロパティとcompositionイベントを活用した2つのアプローチを比較します。
2026.06.19

はじめに

日本語入力(IME)を使ってフォームにテキストを入力しているとき、漢字変換の確定でEnterキーを押した瞬間にフォームが送信されてしまう——そんな経験はないでしょうか?

Next.js + Reactで構築したチャットUIで、まさにこの問題に遭遇しました。日本語でメッセージを入力していると、漢字の候補が下線付きで表示されている段階(変換中)でEnterを押した瞬間、意図せずメッセージが送信されてしまいます。

この記事では、原因の特定から修正方法まで解説します。

環境

  • Next.js 15
  • React 19
  • TypeScript

問題の原因

IMEの変換フローとキーボードイベント

日本語・中国語・韓国語(CJK)を入力する際、OSのIME(Input Method Editor)が介在します。たとえば「東京」と入力するとき:

  1. ローマ字 t, o, k, y, o を打鍵
  2. IMEが「とうきょう」と読みを表示(下線付き)
  3. スペースキーで変換候補を表示
  4. Enterキーで変換を確定

このステップ4の「変換確定のEnter」と、「フォーム送信のEnter」が衝突するのが問題です。

react-ime-composition-pitfalls-ime-flow

ブラウザはどう区別しているか

ブラウザはIMEの変換中かどうかを KeyboardEvent.isComposing プロパティで示します。

状態 isComposing
通常のキー入力 false
IME変換中のキー入力 true

問題のあるコード

function handleKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
  if (e.key === "Enter" && !e.shiftKey) {
    e.preventDefault();
    handleSubmit(); // 変換確定のEnterでも送信されてしまう
  }
}

このコードは isComposing を確認していないため、漢字変換の確定(isComposing: true)でも handleSubmit() が呼ばれてしまいます。

修正方法

e.nativeEvent.isComposing を条件に追加するだけです。

function handleKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
  if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
    e.preventDefault();
    handleSubmit();
  }
}

e.nativeEvent は ReactのSyntheticEventからネイティブのDOMイベントにアクセスするためのプロパティです。isComposing はReactのSyntheticEvent上には存在しないため、e.nativeEvent.isComposing で取得します。

<input> の場合も同様

<textarea> だけでなく <input type="text"> でも同じ問題が発生します。修正方法は同じです:

function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
  if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
    e.preventDefault();
    submitFreeText();
  }
}

応用編:デバウンス自動検索とIME変換の衝突

Enterキーの問題を修正しても、もうひとつ厄介なケースがあります。入力中にデバウンスで自動検索を発火するUIです。

react-ime-composition-pitfalls-debounce-timing

問題

検索フォームでキー入力のたびにデバウンス(例: 1000ms)で自動検索を実行する実装は一般的です。しかし、日本語入力中はIMEの中間文字(下線付きの未確定文字)が入力されるたびに onChange(または shadcn/ui の CommandInput 等が提供する onValueChange)が発火するため、変換を確定していないのにデバウンスコールバックが発火し、中間状態のまま検索が実行されてしまいます。

// 問題のあるコード
const handleChange = (value: string) => {
  setQuery(value);
  // IME変換中でも発火してしまう
  searchDebounce(() => executeSearch(), 1000);
};

解決方法:2つのアプローチ

アプローチ1: useRef でIME変換状態を追跡

compositionstart / compositionend イベントで変換中フラグを useRef で管理し、デバウンス検索をガードします。

// hooks
const isComposingRef = useRef(false);

const handleChange = useCallback(
  (value: string) => {
    setQuery(value);
    if (!isComposingRef.current) {
      searchDebounce(() => executeSearch(), DEBOUNCE_MS);
    }
  },
  [searchDebounce, executeSearch],
);
// コンポーネント
<input
  onChange={(e) => handleChange(e.target.value)}
  onCompositionStart={() => { isComposingRef.current = true; }}
  onCompositionEnd={() => {
    isComposingRef.current = false;
    searchDebounce(() => executeSearch(), DEBOUNCE_MS);
  }}
/>

アプローチ2: onInput イベントの isComposing を直接チェック

useRef を使わず、ネイティブの InputEvent.isComposing をイベントハンドラ内で直接確認します。

// hooks — handleChange から searchDebounce を削除
const handleChange = useCallback(
  (value: string) => {
    setQuery(value);
  },
  [],
);

const triggerSearch = useCallback(() => {
  searchDebounce(() => executeSearch(), DEBOUNCE_MS);
}, [searchDebounce, executeSearch]);
// コンポーネント
<input
  onChange={(e) => handleChange(e.target.value)}
  onInput={(e: React.FormEvent<HTMLInputElement>) => {
    if (!(e.nativeEvent as InputEvent).isComposing) {
      triggerSearch();
    }
  }}
  onCompositionEnd={() => {
    triggerSearch();
  }}
/>

2つのアプローチの比較

useRef onInput イベント
変更量 ~10行 ~15行
影響範囲 小 — 既存フローにガードを追加するだけ 中 — 検索トリガーの責務をコンポーネント側に移動
正確性 同期的に更新されるため競合なし モダンブラウザでは問題なし。Safari 16以前の一部バージョンで compositionend 直後の input イベントの isComposingtrue のままになるケースが報告されている(onCompositionEnd で補完)
テスト容易性 refのモックが容易 ネイティブ InputEvent + CompositionEvent のシミュレーションが必要

フォーム監視(form.watch 等)との競合に注意

react-hook-formの form.watch のように、フォーム全体の変更を監視して自動検索を発火している場合、テキストフィールドの setValue でもwatchが反応し、IME変換中にもかかわらず検索が発火します。 上記のアプローチでテキスト入力のデバウンスを制御していても、watchが別経路で検索をトリガーしてしまうためです。

watchのコールバック内でフィールド名を判定し、テキスト入力の変更をスキップする必要があります。

useEffect(() => {
  const subscription = form.watch((_values, { type, name }) => {
    if (type !== "change" || !name) return;
    // テキスト入力の変更はonInput/onCompositionEndで制御するためスキップ
    if (name === "query") return;
    searchDebounce(() => executeSearch(), DEBOUNCE_MS);
  });
  return () => subscription.unsubscribe();
}, [form, searchDebounce, executeSearch]);

まとめ

Enterキーの問題

修正前 修正後
変換確定Enter フォーム送信される 変換確定のみ
通常Enter フォーム送信される フォーム送信される

修正のポイントは1行だけです:

&& !e.nativeEvent.isComposing

デバウンス自動検索の問題

修正前 修正後
IME変換中の入力 デバウンス後に検索発火 検索スキップ
変換確定後 デバウンス後に検索発火 デバウンス後に検索発火

どちらのアプローチでも修正可能ですが、影響範囲の小ささを重視するなら useRef、副作用を避けてイベントの情報をそのまま使いたいなら onInput イベントのアプローチが適しています。

CJK言語を扱うWebアプリでは、Enterキーの制御だけでなく、デバウンス系の自動実行もIME変換状態を考慮する必要があります。

この記事をシェアする

関連記事