
ReactでIME変換中に起きる2つの落とし穴——Enter送信とデバウンス自動検索
はじめに
日本語入力(IME)を使ってフォームにテキストを入力しているとき、漢字変換の確定でEnterキーを押した瞬間にフォームが送信されてしまう——そんな経験はないでしょうか?
Next.js + Reactで構築したチャットUIで、まさにこの問題に遭遇しました。日本語でメッセージを入力していると、漢字の候補が下線付きで表示されている段階(変換中)でEnterを押した瞬間、意図せずメッセージが送信されてしまいます。
この記事では、原因の特定から修正方法まで解説します。
環境
- Next.js 15
- React 19
- TypeScript
問題の原因
IMEの変換フローとキーボードイベント
日本語・中国語・韓国語(CJK)を入力する際、OSのIME(Input Method Editor)が介在します。たとえば「東京」と入力するとき:
- ローマ字
t,o,k,y,oを打鍵 - IMEが「とうきょう」と読みを表示(下線付き)
- スペースキーで変換候補を表示
- Enterキーで変換を確定
このステップ4の「変換確定のEnter」と、「フォーム送信のEnter」が衝突するのが問題です。

ブラウザはどう区別しているか
ブラウザは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です。

問題
検索フォームでキー入力のたびにデバウンス(例: 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 イベントの isComposing が true のままになるケースが報告されている(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変換状態を考慮する必要があります。







