◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-04-22中級

Rorkで作ったアプリの日本語入力が『変換中に暴発する』問題を根本解決する

Rorkで生成したアプリの日本語入力で、IME変換中にイベントが暴発する、候補ウィンドウが閉じる問題を根本から解決します。デバウンスでの吸収、Enter確定パターン、multilineでの分離という3つの解決策と、Rorkへ追加指示するプロンプト例を紹介します。

Rork584React Native239日本語入力IMETextInput検索ボックストラブルシューティング85

リリースから数日後、レビュー欄に「変換確定できないバグあり」という星1の一行を見つけて、胃が痛くなりました。Rorkで作ったメモアプリです。手元の英語キーボードでは何事もなく動いていたので、最初は原因の見当すらつきませんでした。

「日本語を入力すると挙動がおかしい」「変換中に文字が消える」「検索ボックスがガタガタする」——日本のユーザーから届くこの種の声は、たいてい同じ根から来ています。生成されたコードが、日本語IME(変換)の存在を前提にしていないのです。

英語圏の開発者がテストしただけでは見つかりにくい落とし穴なので、Rorkの生成コードでよく見かけるパターンを題材に、React NativeのTextInputと日本語IMEの相性を整理します。手元のコードをその日のうちに直せるよう、手順は短く区切ってお伝えします。

なぜ変換中にイベントが暴発するのか

React NativeのTextInputのonChangeTextは、英語のようなアルファベット入力を前提に設計されています。1文字打つたびに確定文字として扱われ、即座に親コンポーネントへ通知が飛びます。

ところが日本語IMEでは、とくにiOSの日本語キーボードで、変換確定前の「未確定文字列」(ひらがなで表示されて下線が引かれている状態)も、OSからは入力イベントとして飛んできます。つまり「こんにちは」と打って変換候補を選ぶまでの間に、onChangeTextが5〜10回以上発火してしまうのです。

このイベント暴発が、次のような現象を引き起こします。

  • 検索ボックスでAPIリクエストが1文字ごとに飛び、変換候補が出るたびにネットワーク負荷とレート制限エラーが起きます
  • Zustandなどで状態更新すると、その再描画が変換候補ウィンドウを強制的に閉じてしまいます
  • キーボードや端末によっては、変換を確定したつもりのEnterでonSubmitEditingが発火し、意図しない画面遷移が起きます

一方でWebのHTMLでは、compositionstart・compositionupdate・compositionendというイベントで変換中の状態を明示的に追跡できます。React Nativeにはこれに対応する仕組みが用意されていないため、自前で工夫する必要があります。

症状を再現する最小コード

まずは問題がどのように現れるかを確認できる、最小の再現コードを紹介します。日本語環境のシミュレータまたは実機で試してみてください。

import { useState } from "react";
import { TextInput, View, Text } from "react-native";
 
export default function BrokenSearchBox() {
  const [query, setQuery] = useState("");
  const [callCount, setCallCount] = useState(0);
 
  // ❌ 悪い例:変換中も含めて毎回呼ばれる
  const handleChange = (text: string) => {
    setQuery(text);
    setCallCount((c) => c + 1);
    // 本来はここで検索APIを呼ぶ想定
    console.log("検索API呼び出し:", text);
  };
 
  return (
    <View style={{ padding: 24 }}>
      <TextInput
        value={query}
        onChangeText={handleChange}
        placeholder="検索..."
        style={{ borderWidth: 1, padding: 12, borderRadius: 8 }}
      />
      <Text>API呼び出し回数: {callCount}</Text>
    </View>
  );
}

このコードで「東京駅」と入力しようとすると、確定までに callCount が20回前後まで跳ね上がります。1回の検索で20回のAPIリクエストを飛ばす検索ボックスは、率直に言って本番では使えません。

解決策1:デバウンスで暴発を吸収する

最もシンプルな対策は、連続した変化を「ある程度まとまってから」処理することです。いわゆるデバウンス(debounce)と呼ばれる技法で、IMEの仕様を知らなくても症状を緩和できます。

import { useState, useRef, useEffect } from "react";
import { TextInput, View, Text } from "react-native";
 
export default function DebouncedSearchBox() {
  const [inputValue, setInputValue] = useState("");
  const [committedQuery, setCommittedQuery] = useState("");
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
  useEffect(() => {
    // 300ms変更がなければ「確定した」とみなしてAPIへ
    if (timerRef.current) clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => {
      setCommittedQuery(inputValue);
    }, 300);
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, [inputValue]);
 
  useEffect(() => {
    if (committedQuery.length === 0) return;
    // ここで実際の検索APIを呼ぶ
    console.log("検索API呼び出し(確定後):", committedQuery);
  }, [committedQuery]);
 
  return (
    <View style={{ padding: 24 }}>
      <TextInput
        value={inputValue}
        onChangeText={setInputValue}
        placeholder="検索..."
        style={{ borderWidth: 1, padding: 12, borderRadius: 8 }}
      />
      <Text>確定クエリ: {committedQuery}</Text>
    </View>
  );
}

ポイントは、入力欄のstate(inputValue)とAPIへ投げるstate(committedQuery)を分けているところです。入力欄はIMEの変換中も含めて自由に更新させつつ、300ms静止したときだけAPIを呼ぶ設計にします。

入力欄に見えている文字列と、外へ送る文字列は、別の状態として持つ——この分け方が、このあとの対策すべての土台になります。

この方法は完璧ではありませんが、Rorkで生成されたコードをそのまま活かしたまま最小の修正で済むのが利点です。APIリクエストの過剰発火を防ぎたい検索ボックスには、まずこの形を試してみてください。

解決策2:Enterキーでのみ確定するパターン

リアルタイム検索にこだわらないのであれば、「Enterキー押下まで検索しない」という割り切った設計も有効です。むしろこちらのほうが、ユーザーが検索の意思表示をしたタイミングで動くため、UXとしては素直なことが多いです。

import { useState } from "react";
import { TextInput, View, Text } from "react-native";
 
export default function SubmitBasedSearchBox() {
  const [inputValue, setInputValue] = useState("");
  const [searchResult, setSearchResult] = useState<string[]>([]);
 
  const handleSubmit = () => {
    // このタイミングでは確実に変換確定済み
    console.log("検索実行:", inputValue);
    // モック検索結果
    setSearchResult([`${inputValue}の結果1`, `${inputValue}の結果2`]);
  };
 
  return (
    <View style={{ padding: 24 }}>
      <TextInput
        value={inputValue}
        onChangeText={setInputValue}
        onSubmitEditing={handleSubmit}
        returnKeyType="search"
        placeholder="検索語を入力してEnter"
        style={{ borderWidth: 1, padding: 12, borderRadius: 8 }}
      />
      {searchResult.map((r, i) => (
        <Text key={i}>{r}</Text>
      ))}
    </View>
  );
}

onSubmitEditingは、キーボードの「改行」や「検索」キーが押されたときだけ発火します。returnKeyType="search"を設定するとキーボードに虫眼鏡アイコンが表示され、ユーザーに「ここで検索が実行される」ことが伝わりやすくなります。

私はRorkで作ったアプリをApp Storeにリリースする際、この方式に切り替えたあとはIME関連のクレームがぱったり消えました。個人的には、検索ボックスはこちらのパターンをデフォルトにすることをおすすめします。

解決策3:multiline TextInputで改行と確定を分離する

メモアプリや日記アプリのように、multiline(複数行入力)のテキストエリアを使う場合は、また別の問題が発生します。日本語IMEの確定キーと改行キーが混線して、意図しないタイミングで改行が挿入されるケースです。

これを防ぐには、onKeyPressで物理キーを監視するのではなく、UIレベルで「保存ボタンを明示的に設ける」設計にしてしまうのが現実的です。

import { useState } from "react";
import { TextInput, View, Text, Pressable } from "react-native";
 
export default function MultilineMemoEditor() {
  const [memo, setMemo] = useState("");
  const [saved, setSaved] = useState(false);
 
  const handleSave = () => {
    // 保存ボタンが押されたときのみ永続化
    console.log("メモを保存:", memo);
    setSaved(true);
  };
 
  return (
    <View style={{ padding: 24 }}>
      <TextInput
        value={memo}
        onChangeText={(t) => {
          setMemo(t);
          setSaved(false);
        }}
        multiline
        placeholder="メモを入力..."
        style={{
          borderWidth: 1,
          padding: 12,
          borderRadius: 8,
          minHeight: 120,
          textAlignVertical: "top",
        }}
      />
      <Pressable
        onPress={handleSave}
        style={{
          marginTop: 16,
          padding: 12,
          backgroundColor: "#0066cc",
          borderRadius: 8,
        }}
      >
        <Text style={{ color: "white", textAlign: "center" }}>保存</Text>
      </Pressable>
      {saved && <Text style={{ marginTop: 8 }}>保存しました</Text>}
    </View>
  );
}

multilineなTextInputで「Enterキーで保存」にしたい気持ちはわかるのですが、日本語の変換確定のためにもEnterキーを使う仕様上、どうしても競合します。ユーザーに明示的な保存アクションを求めるデザインのほうが、結果的にトラブルが少ないです。

変換中の value を加工しない

デバウンスでも防げない落とし穴が、もうひとつあります。onChangeText の中で入力文字列をその場で加工し、そのまま value に戻してしまう書き方です。

// ❌ 変換中の文字列まで削ったり置き換えたりすると、候補ウィンドウが閉じることがあります
<TextInput
  value={value}
  onChangeText={(t) => setValue(t.replace(/\s/g, "").slice(0, 20))}
/>

空白の除去や文字数の切り詰めは、確定したあとにまとめて行うほうが安全です。入力中は受け取った文字列をそのまま保持し、onEndEditing か保存ボタンを押した時点で整えます。いまは「見えている文字列には手を触れず、整えるのは確定してから」という線引きで実装するようにしています。

三つの対策の使い分け

どれを選ぶかは、画面が「入力しながら結果を見る」のか「入力してから決定する」のかで決まります。

対策向いている画面気をつけたい点
デバウンス(300ms前後)入力に合わせて候補を絞るライブ検索待ち時間のぶん反応が遅れます。変換が長引くと、途中の文字で検索が走ることもあります
onSubmitEditingで確定検索キーを押して結果を見る一般的な検索ライブ検索の手応えは失われます
明示的な保存ボタンメモや日記などのmultiline入力ボタンの位置とキーボードの重なりを実機で確かめます

Rorkに追加で指示すべきプロンプト例

Rorkでアプリを最初から作る際、日本語ユーザー向けであることを明示的に伝えると、生成コードの質が変わってきます。私が実際に使っているプロンプトの追記例は次のようなものです。

  • 「TextInputを使う画面は、日本語IMEの変換中にonChangeTextが暴発することを考慮して、debounceまたはonSubmitEditingで処理を確定させる設計にしてください」
  • 「検索ボックスは、入力中は検索APIを呼ばず、returnKeyType="search"のEnterキー押下時にのみ検索を実行してください」
  • 「multilineのTextInputでは、Enterキーで保存する設計を避け、明示的な保存ボタンを設けてください」

AIにこうした前提を伝えるだけで、後から日本語特有のバグに悩まされる時間を大幅に減らせます。Rorkのプロンプト設計についてはRorkのプロンプトエンジニアリング実践ガイドでより体系的に触れていますので、あわせて読んでみてください。

日本語入力で気づきにくい3つの落とし穴

最後に、Rorkで作ったアプリを日本語環境でテストするときに、見落としがちなポイントを3つ挙げておきます。

  1. maxLengthが「未確定文字列」も含めて数える:maxLength=10にしたTextInputで、変換前のひらがなが長く伸びると、確定後の漢字が入らなくなることがあります(端末やキーボードによって挙動は異なります)。制限はやや緩めに設定し、確定後に切り詰めるのが無難です。
  2. 絵文字の結合文字対応:家族絵文字(👨‍👩‍👧‍👦)のような絵文字は、複数のコードポイントをつないだ並びです。text.lengthで数えると見た目と合わず、[...text].lengthでもコードポイント単位なので、まだ1文字にはなりません。見た目どおりに数えたいときは、Intl.Segmenterが使える環境ならそれを、使えない環境ではgrapheme単位で分割するライブラリを検討してください。
  3. 全角英数字と半角英数字の混在:パスワードフィールドやメールアドレス欄では、autoCapitalize="none"とkeyboardType="email-address"を明示しておかないと、iOSで全角入力モードに入ったまま打ってしまうユーザーが発生します。

次の一歩

まずはお手元のRorkプロジェクトの中から、TextInputを使っている画面をひとつ選んで、onChangeTextに直接APIコールやstate更新の重い処理を書いていないか確認してみてください。もし書いているなら、この記事のデバウンスまたはonSubmitEditingパターンへ差し替えることで、日本語ユーザーの体感品質が目に見えて変わります。

フォーム周辺の実装をさらに磨き込みたい方は、react-hook-formとZodで作るRorkのフォームバリデーション、キーボード表示に関するUI問題はRorkアプリでキーボードが入力フィールドを隠す問題の対処ガイドも参考にしてみてください。日本語で入力する方が最初に触れるのは、機能の多さより、変換がすんなり通るかどうかなのだと思います。その手触りだけは、リリース前に必ず自分の指で確かめるようにしています。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-05-18
Rork で Linking.canOpenURL が iOS だけ false を返す ─ LSApplicationQueriesSchemes 設定漏れの直し方
TikTok や X、LINE 公式アカウントへ飛ばす実装が iOS の本番ビルドだけで動かない症状を、LSApplicationQueriesSchemes の追加と EAS Build の再生成手順で確実に解消します。
⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。