JavaScript

IME変換中のEnter送信を防ぐ|isComposingとReact・Vueの実装

日本語の変換確定でチャットが送られる問題を、isComposing・compositionイベント・229の補助判定で切り分けます。JavaScript、React、Vueのtextarea実装と実機確認表を掲載。

この記事の目次
  1. isComposingの判定はpreventDefaultより先に置く
  2. まずは素のJavaScriptで、送信回数を見えるようにする
  3. ReactではnativeEventを確認し、変換フラグをrefに置く
  4. Vueではkeydown.enter.preventに任せず、関数内で順序を決める
  5. 日本語IMEで「確定」と「次のEnter」を分けて試す
  6. 今回の検証結果と、実機で残る確認

日本語の変換を確定するEnterでチャットが送られるときは、keydownでIMEの入力を先に判定し、その後で送信用のEnterを処理します。compositionendを送信の合図にしたり、Enterなら無条件に送信したりしないようにします。

以下は、textareaで「Enterは送信、Shift+Enterは改行」にする実装です。変換中のキー入力はIMEへ任せ、通常のEnterだけを自分の送信処理につなぎます。実際の通信の代わりに、画面へ送信内容と回数を表示して確かめます。

isComposingの判定はpreventDefaultより先に置く

KeyboardEvent.isComposingは変換セッション中かどうかを表します。ただし、変換開始・終了の境界ではイベント順序によりfalseになる場合があります。MDNのkeydownとIMEの説明では、IMEが処理したキーを示すkeyCode === 229も補助として確認する例が示されています。

まず共通関数をenter-handler.mjsへ保存します。ReactではnativeEvent、通常のJavaScriptとVueでは受け取ったイベント自体からネイティブの値を読みます。

export function handleChatEnter(event, composing, submit) {
  const native = event.nativeEvent ?? event;
  // keyCodeは非推奨だが、IME境界での229判定だけに限定して補助する。
  if (composing || native.isComposing || native.keyCode === 229) return;
  if (event.key !== 'Enter') return;
  if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return;
  event.preventDefault();
  if (event.repeat) return;
  submit();
}

keyCodeは非推奨です。通常のEnter判定はkey === 'Enter'を使い、229はIME境界の補助だけに限定しています。これで全OS・全IMEの動作を保証できるわけではないため、後述の実機確認も行います。

変換中は、送信関数を呼ばずにreturnします。ここで先にpreventDefault()を実行すると、変換や確定などの標準動作まで妨げる可能性があります。通常のEnterだと判断した後で改行を止め、長押しのrepeatなら送信しません。

スポンサーリンク

まずは素のJavaScriptで、送信回数を見えるようにする

次の2ファイルを共通関数と同じ場所へ保存し、ローカルWebサーバーから開きます。HTMLには入力欄のラベル、操作説明、明示した送信ボタンを置きます。

<!doctype html>
<html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>IMEとEnterの確認</title>
<style>body{font:18px/1.7 sans-serif;max-width:44rem;margin:2rem auto;padding:0 1rem}textarea{display:block;box-sizing:border-box;width:100%;font:inherit}button{font:inherit;margin-top:1rem}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style>
<form id="chat">
  <label for="message">メッセージ</label>
  <textarea id="message" name="message" rows="4" required aria-describedby="hint"></textarea>
  <p id="hint">Enterで送信、Shift+Enterで改行。変換中は送信しません。</p>
  <button type="submit">送信</button>
</form>
<p role="status" id="status">送信回数:0</p>
<pre id="sent"></pre>
<script type="module" src="./main.mjs"></script>
</html>
import { handleChatEnter } from './enter-handler.mjs';
const form = document.querySelector('#chat');
const field = document.querySelector('#message');
const button = form.querySelector('button');
let composing = false;
let sentCount = 0;
field.addEventListener('compositionstart', () => { composing = true; });
field.addEventListener('compositionend', () => { composing = false; });
field.addEventListener('blur', () => { composing = false; });
field.addEventListener('keydown', event => {
  handleChatEnter(event, composing, () => form.requestSubmit(button));
});
form.addEventListener('submit', event => {
  event.preventDefault();
  if (composing || !field.value.trim()) return;
  document.querySelector('#sent').textContent = field.value;
  document.querySelector('#status').textContent = `送信回数:${++sentCount}`;
  field.value = '';
  field.focus();
});

compositionstartでフラグを立て、compositionendで下ろします。確定後のEnterが229として届く境界は、共通関数側の補助判定で送信対象から外します。compositionendそのものでは送信しません。

送信はrequestSubmit()を通し、Enterとボタンの処理をsubmitへ集めます。requiredによる入力検証も通ります。送信先を持つフォームの基本はHTMLフォームの作り方で確認できます。

このデモではtextContentへ内容を表示するだけです。実際にAPIへ送る場合は、処理中フラグ・再送制御・サーバー側検証を追加し、成功を確認してから入力を消します。ここでの同期処理をそのまま通信成功の判定にしないでください。

ReactではnativeEventを確認し、変換フラグをrefに置く

Reactのイベントから元のDOMイベントを参照するにはnativeEventを使えます。以下は、親から受け取る同期関数onSendへ文字列を渡すコンポーネントです。共通のenter-handler.mjsを同じフォルダへ置きます。

import React, { useRef } from 'react';
import { handleChatEnter } from './enter-handler.mjs';

export default function ChatInput({ onSend }) {
  const composing = useRef(false);
  function submit(event) {
    event.preventDefault();
    const form = event.currentTarget;
    const value = new FormData(form).get('message');
    if (composing.current || typeof value !== 'string' || !value.trim()) return;
    onSend(value);
    form.reset();
  }
  return (
    <form onSubmit={submit}>
      <label>メッセージ
        <textarea name="message" required
          onCompositionStart={() => { composing.current = true; }}
          onCompositionEnd={() => { composing.current = false; }}
          onBlur={() => { composing.current = false; }}
          onKeyDown={event => handleChatEnter(event, composing.current,
            () => event.currentTarget.form.requestSubmit())}
        />
      </label>
      <p>Enterで送信、Shift+Enterで改行</p>
      <button type="submit">送信</button>
    </form>
  );
}

この例は入力値をDOMへ任せる非制御フォームです。画面描画が不要な変換中フラグはuseRefで持ち、onKeyDownが現在値を読みます。制御フォームへ移す場合は、入力値のstateとクリア処理もそろえます。基本のvalue/onChangeはReactのinput・textareaの実装で確認できます。

Reactのイベントの詳細は公式の共通コンポーネントリファレンスを参照できます。onKeyDownから直接onSendを呼ぶ経路を追加せず、ボタンと同じsubmitを通す形を保ちます。

Vueではkeydown.enter.preventに任せず、関数内で順序を決める

Vueでも共通関数を使えます。@keydown.enter.preventで先に標準動作を止めるのではなく、@keydownで受け取り、IMEの確認後に必要なときだけ止めます。

<script setup>
import { ref } from 'vue';
import { handleChatEnter } from './enter-handler.mjs';
const emit = defineEmits(['send']);
const composing = ref(false);
function submit(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const value = new FormData(form).get('message');
  if (composing.value || typeof value !== 'string' || !value.trim()) return;
  emit('send', value);
  form.reset();
}
function keydown(event) {
  handleChatEnter(event, composing.value, () => event.currentTarget.form.requestSubmit());
}
</script>
<template>
  <form @submit="submit">
    <label>メッセージ
      <textarea name="message" required
        @compositionstart="composing = true" @compositionend="composing = false"
        @blur="composing = false" @keydown="keydown"></textarea>
    </label>
    <p>Enterで送信、Shift+Enterで改行</p>
    <button type="submit">送信</button>
  </form>
</template>

親は@sendで文字列を受け取ります。この例も値はDOMから読み、同期の表示デモを想定しています。v-modelで値を管理する構成へ移すなら、DOMのresetだけでなくモデル側の値も更新してください。Vueのキー修飾子とイベント修飾子の意味は公式イベント処理ガイドで確認できます。

日本語IMEで「確定」と「次のEnter」を分けて試す

再現確認では日本語入力を有効にし、「にほんご」と入力して変換候補を出します。まずEnterを1回押して「日本語」を確定し、その時点の送信回数が増えていないかを見ます。次に、キーを離してからもう一度Enterを押し、初めて送信されるかを確かめます。

操作 期待すること
未確定文字を変換し、Enterで確定 文字が確定し、送信回数は増えない
確定後、改めてEnter 1回だけ送信される
候補を選び直す/Escで変換を取り消す 送信されず、入力を続けられる
通常入力でShift+Enter 改行され、送信されない
Enterを押し続ける repeatによる送信が増えない
確定後に送信ボタンを押す Enterと同じ送信処理へ進む
空欄/空白だけで送信 送信されない
変換途中でフォーカスを外し、戻る 変換状態が残って通常送信を妨げない

確認表にはOS、ブラウザ名と版、IME名と版、操作、期待値、実際の結果を残します。WindowsのMicrosoft IMEやGoogle日本語入力、macOSの日本語入力、スマートフォンのキーボードは別の条件として確認します。fill()で日本語文字列を入れただけでは、OSの変換候補や確定操作の検証になりません。

変換終了後の一定時間だけEnterを無効にする対策は、入力速度によって「確定後に押した本当の送信」まで落とすことがあります。固定の100msなどを万能の値として追加せず、実際に届いたイベント順序を確認して原因を絞ります。誤送信の影響が大きい入力欄なら、Enterを改行にして送信ボタンだけにする操作設計も選べます。

今回の検証結果と、実機で残る確認

2026年9月12日にChrome 152、React 19.3.0、Vue 3.5.42で3つのデモを確認しました。通常のEnter・Shift+Enter・ボタン操作に加え、人工的に発火した変換中イベント、229、repeatで送信されないこと、空白だけでは送れないことを確かめています。

OSの日本語IMEを使った変換操作、Safari・Firefox・モバイル実機の組み合わせは未検証です。人工イベントの分岐テストと、実際の入力体験を分けて確認してください。まず誤送信が起きた環境で「確定のEnter」と「次のEnter」を再現し、送信回数を見ながら直すのが出発点です。

スポンサーリンク