DEV Community

mi-inu
mi-inu

Posted on

Why 1.6 Billion East Asians Are Quietly Raging at Your Enter Key Handler

TL;DR: Your Enter key handler is broken for 1.6 billion people using IME (Input Method Editor). When we press Enter to select a character during text conversion, your chat app sends a half-written message. Fix it with event.isComposing check. Please. We're begging you.

Hey there, I'm Japanese.

In Japan, we're basically taught from elementary school that suppressing anger and being polite is a virtue, so we develop this wonderful habit of bottling everything up until we absolutely explode. But today I've had a few drinks, so let me be honest with you.

There's a reason almost every East Asian is pissed off at you ASCII-language developers.

AI chat is all the rage these days, right? And when you think "chat," the normal UI is: press Enter in the text field, message sends. Simple. So you write something like this:

import React, { useCallback, useRef } from 'react';

export function InputField({ onSubmit }) {
  const el = useRef();
  const handleKeydown = useCallback(
    event => {
      if (event.key === 'Enter') {
        onSubmit(el.current.value);
      }
    },
    [onSubmit]
  );

  return <input ref={el} onKeyDown={handleKeydown} />;
}
Enter fullscreen mode Exit fullscreen mode

Yeah, that's it. Simple, clean, perfectly bug-free implementation. It passes your tests, passes E2E, passes QA, gets deployed. And then East Asians lose their minds completely.

Why?

IME. Input Method Editor. Conversion. Composition. Whatever you want to call this nightmare.

I'm Japanese, so let me explain Japanese. We use hiragana, katakana, and about 2,000 commonly-used kanji characters. Meanwhile, your typical keyboard has what, 104 keys?

Obviously not enough. A-Z gives you 26 characters, but あ through ん alone is 46 characters. Why the hell do we use so many characters? Because Japanese people wanted to pack every ounce of emotion into their text. "Yoroshiku" (よろしく), "YOROSHIKU" (ヨロシク), "Yoroshiku" (宜しく), and "YO-RO-SHI-KU" (夜露死苦) all read as "yo-ro-shi-ku." よろしく is orthodox and friendly. ヨロシク means the person is either quirky or keeping their distance. 宜しく? That's a middle-aged salaryman. 夜露死苦? That's definitely a punk-fashion kid wreaking havoc on a huge motorcycle.

Anyway, Japanese people care deeply about character representation even for the same sounds.

So first, we type in ASCII to get phonetic hiragana, then we select from candidate characters that match those sounds.

Compositing: Yoroshiku

This image shows exactly what's happening during composition. The underlined part is unconfirmed text, with candidates shown below. It even thoughtfully shows similar emoji and the meaning of each kanji character.

To select the next candidate down, here's the key sequence visualized:

y o r o s h i k u [Space] [Enter]

During conversion, we use [Up] [Down] [Space] [Enter] [Esc] keys.
This happens roughly every 1-3 words. Multiple times per minute of typing.

The Nightmare in Action

Now, let's look back at that first implementation:

import React, { useCallback, useRef } from 'react';

export function InputField({ onSubmit }) {
  const el = useRef();
  const handleKeydown = useCallback(
    event => {
      if (event.key === 'Enter') {
        onSubmit(el.current.value);
      }
    },
    [onSubmit]
  );

  return <input ref={el} onKeyDown={handleKeydown} />;
}
Enter fullscreen mode Exit fullscreen mode

What happens when you implement this? Simple. The moment we try to select a character variant, the message sends.
It's like sending "I " or "Describe this" or "Hi, I'm " in English.
And since most chat apps reset the input field on send, our sentence never gets completed. So Japanese users quietly seethe with rage, open Sublime Text, and start typing there instead.

This happens in SO MANY services originating from ASCII-land. I've sent bug reports and fixes to countless services, but it never ends. Claude Code's 9/30 update had this exact problem, and tons of Japanese users are pissed.

Here's another fun example.
Let's say you want to pick up input values and save them. Debounce the keydown event and save after xx ms. Simple, innocent logic.

import React, { useCallback, useRef, useEffect } from 'react';

export function InputField({ onSubmit, onSave }) {
  const el = useRef();
  const timeoutRef = useRef();

  const handleKeydown = useCallback(
    event => {
      if (event.key === 'Enter') {
        onSubmit(el.current.value);
      } else {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = setTimeout(() => {
          onSave(el.current.value);
        }, 2000);
      }
    },
    [onSubmit, onSave]
  );

  useEffect(() => {
    return () => clearTimeout(timeoutRef.current);
  }, []);

  return <input ref={el} onKeyDown={handleKeydown} />;
}
Enter fullscreen mode Exit fullscreen mode

This doesn't cause problems by itself. But when upstream re-rendering happens and the element gets destroyed and recreated, absolute chaos ensues. Mid-conversion, the text suddenly disappears and reverts to an earlier state, or you get garbage hiragana stuck in the field. This actually happened in ChatGPT's macOS app.

Writing is thought organization and output. These subtle harassment-level UX issues completely destroy a product's appeal.

ASCII-language folks can't know this, can't notice this. That's why I'm writing this article today—we need to keep speaking up about this.

The Fix That Will Save You From 1.6 Billion Enemies

So how do we fix this? There's this incredibly convenient property called event.isComposing.
It tells you if conversion is happening right now. Simple bugs like saving mid-conversion can be prevented with this.

export function InputField({ onSubmit, onSave }) {
  const el = useRef();
  const timeoutRef = useRef();

  const handleKeydown = useCallback(
    event => {
      if (event.key === 'Enter') {
        onSubmit(el.current.value);
      } else {
        clearTimeout(timeoutRef.current);
        if(!event.nativeEvent.isComposing){
          timeoutRef.current = setTimeout(() => {
            onSave(el.current.value);
          }, 2000);
        }
      }
    },
    [onSubmit, onSave]
  );

  useEffect(() => {
    return () => clearTimeout(timeoutRef.current);
  }, []);

  return <input ref={el} onKeyDown={handleKeydown} />;
}
Enter fullscreen mode Exit fullscreen mode

The Safari Special Hell Edition

But isComposing has a huge problem: in Safari, it becomes false on the keydown event the moment Enter is pressed. This behavior has been ignored for ages, and if any Apple engineers are reading this, please create an issue immediately.

Anyway, there's a proper way to work around this and a cheap trick. The easy one: during IME conversion, the keyCode for Enter becomes 229 instead of 13.

import React, { useCallback, useRef } from 'react';

export function InputField({ onSubmit }) {
  const el = useRef();
  const handleKeydown = useCallback(
    event => {
      if (event.key === 'Enter' && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229) {
        onSubmit(el.current.value);
      }
    },
    [onSubmit]
  );

  return <input ref={el} onKeyDown={handleKeydown} />;
}
Enter fullscreen mode Exit fullscreen mode

Or, more complex but modern approach:

import React, { useCallback, useRef } from 'react';

export function InputField({ onSubmit }) {
  const el = useRef();
  const isComposingRef = useRef(false);

  const handleKeydown = useCallback(
    event => {
      if (event.key === 'Enter' && !isComposingRef.current) {
        onSubmit(el.current.value);
      }
    },
    [onSubmit]
  );

  const handleCompositionStart = useCallback(() => {
    isComposingRef.current = true;
  }, []);

  const handleCompositionEnd = useCallback(() => {
    isComposingRef.current = false;
  }, []);

  return (
    <input 
      ref={el} 
      onKeyDown={handleKeydown}
      onCompositionStart={handleCompositionStart}
      onCompositionEnd={handleCompositionEnd}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Please, For the Love of All That Is Holy

That's it. That's literally it. This tiny bit of code prevents you from earning the hatred of 1.6 billion East Asians.

Don't capture Enter, Esc, Up, or Down during isComposing. Just that. That's all it takes to save countless people from suffering.

Oh, and by the way—Slack and Discord figured this out years ago. But somehow every new service steps on the exact same rake. Chinese developers with Pinyin input and Korean developers with Hangul composition are screaming alongside us.

Go add this to your project's AGENTS.md right now: "When handling Enter in text fields, reference isComposing with Safari considerations." Better yet, add to your QA checklist: "When processing Enter, test with multi-byte character IME."

This is all we East Asians wanted to say.
I'm dead serious. Please.

P.S. After reading this article, go buy coffee for the East Asian developer sitting next to you. They're probably silently patching your code as we speak.

Top comments (0)