← ./articles

React IME Input Bugs: Guard Key Handlers During Composition

If your React search box, command palette, autocomplete, or rename field works in English but behaves strangely with Japanese input, check your keyboard handlers.

IME users do not type final characters one key at a time. They compose text first, then commit it. During composition, browsers still fire keyboard events. If your component treats Enter, ArrowDown, or Escape as normal shortcuts during that phase, it can select the wrong item, submit half-written text, or close a dropdown before the user is done.

The symptom

A typical broken autocomplete does this:

function SearchBox() {
  const [selectedIndex, setSelectedIndex] = useState(0);

  function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
    if (event.key === "ArrowDown") {
      setSelectedIndex((index) => index + 1);
      event.preventDefault();
    }

    if (event.key === "Enter") {
      submitSelectedItem();
    }
  }

  return <input onKeyDown={handleKeyDown} />;
}

This works for simple Latin keyboard input. With Japanese IME composition, those key events may fire before the text is committed.

Guard while composition is active

Check isComposing and the legacy keyCode === 229 signal:

function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
  if (event.nativeEvent.isComposing || event.keyCode === 229) {
    return;
  }

  if (event.key === "ArrowDown") {
    setSelectedIndex((index) => Math.min(index + 1, items.length - 1));
    event.preventDefault();
  }

  if (event.key === "Enter") {
    submitSelectedItem();
    event.preventDefault();
  }
}

isComposing is the readable modern signal. keyCode === 229 is still useful as a compatibility fallback because IME-related key events can report special key data in some browser and OS combinations.

Process final text after compositionend

If your component needs to run search or validation after the user commits text, handle composition end:

function SearchBox() {
  const [query, setQuery] = useState("");

  return (
    <input
      value={query}
      onChange={(event) => setQuery(event.currentTarget.value)}
      onCompositionEnd={(event) => {
        setQuery(event.currentTarget.value);
        runSearch(event.currentTarget.value);
      }}
      onKeyDown={handleKeyDown}
    />
  );
}

Do not use keydown as the only source of truth for text. Let input/change and composition events own the text value. Let keydown handle navigation only when composition is not active.

Apply the guard to every keyboard shortcut near text input

The bug often survives because one input gets fixed and another one does not.

Audit these components:

  • Search boxes
  • Autocomplete dropdowns
  • Command palettes
  • Rename fields
  • Tag editors
  • Chat inputs
  • Editable table cells
  • Form fields where Enter submits

Any shortcut that changes selection, commits a value, submits a form, or closes a popup should skip while composition is active.

Do not break accessibility while fixing IME

IME support and keyboard accessibility are not opposites. The right rule is:

During composition:
  text composition owns the keys

After composition:
  component keyboard shortcuts apply

Keep normal keyboard support for non-composition states. Arrow keys should still move through a list after the user commits the text. Enter should still submit once the input is stable.

Test cases

Use a real IME to verify:

  1. Type Japanese text into the input.
  2. Press arrow keys while the candidate window is open.
  3. Confirm the app dropdown does not move prematurely.
  4. Commit the IME text.
  5. Press arrow keys again.
  6. Confirm the app dropdown now moves normally.
  7. Press Enter after commit.
  8. Confirm the app submits the final text, not the preedit text.

Automated tests can cover the handler logic, but this class of bug deserves at least one manual check with a real IME because browser event ordering is the point.

References

Summary

If a React input handles text and keyboard shortcuts, it must respect IME composition. Guard keydown logic with isComposing and keyCode === 229, process final text after composition, and test the actual Japanese input path before shipping.