Rorkで作ったアプリを一度でも日本のユーザーに使ってもらったことがある方なら、おそらく一度は同じフィードバックを受けた経験があるはずです。「日本語を入力すると挙動がおかしい」「変換中に文字が消える」「検索ボックスがガタガタする」——英語圏のAI開発ツールで生成されたコードが、そのままでは日本語の変換(IME)を想定していないために起きる問題です。
私自身、Rorkで作ったメモアプリをApp Storeに出した直後、レビュー欄に「変換確定できないバグあり、星1」というコメントを見つけてかなり焦りました。コード自体は問題なく動いていたのですが、英語圏の開発者がテストしただけでは見つからない、日本語特有の落とし穴だったのです。
Rorkの生成コードでよく見かけるパターンを題材に、React NativeのTextInputと日本語IMEの相性問題を根本から整理し、手元のコードを今日中に修正できる具体的な手順を順番にご紹介します。
なぜ変換中にイベントが暴発するのか
React NativeのTextInputのonChangeTextは、英語のようなアルファベット入力を前提に設計されています。1文字打つたびに確定文字として扱われ、即座に親コンポーネントへ通知が飛びます。
ところが日本語IMEでは、変換確定前の「未確定文字列」(ひらがなで表示されて下線が引かれている状態)も、OSからは入力イベントとして飛んできます。つまり「こんにちは」と打って変換候補を選ぶまでの間に、onChangeTextが5〜10回以上発火してしまうのです。
このイベント暴発が、次のような現象を引き起こします。
- 検索ボックスでAPIリクエストが1文字ごとに飛び、変換候補が出るたびにネットワーク負荷とレート制限エラーが発生する
- Zustandなどで状態更新すると、その再描画が変換候補ウィンドウを強制的に閉じてしまう
- 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キーを使う仕様上、どうしても競合します。ユーザーに明示的な保存アクションを求めるデザインのほうが、結果的にトラブルが少ないです。
Rorkに追加で指示すべきプロンプト例
Rorkでアプリを最初から作る際、日本語ユーザー向けであることを明示的に伝えると、生成コードの質が変わってきます。私が実際に使っているプロンプトの追記例は次のようなものです。
- 「TextInputを使う画面は、日本語IMEの変換中にonChangeTextが暴発することを考慮して、debounceまたはonSubmitEditingで処理を確定させる設計にしてください」
- 「検索ボックスは、入力中は検索APIを呼ばず、
returnKeyType="search"のEnterキー押下時にのみ検索を実行してください」 - 「multilineのTextInputでは、Enterキーで保存する設計を避け、明示的な保存ボタンを設けてください」
AIにこうした前提を伝えるだけで、後から日本語特有のバグに悩まされる時間を大幅に減らせます。Rorkのプロンプト設計についてはRorkのプロンプトエンジニアリング実践ガイドでより体系的に触れていますので、あわせて読んでみてください。
日本語入力で気づきにくい3つの落とし穴
最後に、Rorkで作ったアプリを日本語環境でテストするときに、見落としがちなポイントを3つ挙げておきます。
- maxLengthが「未確定文字列」も含めて数える:
maxLength=10にしたTextInputで、変換前のひらがな7文字が入った状態では確定後の漢字3文字が入らなくなる、という現象が起こります。制限はやや緩めに設定しておくのが無難です。 - 絵文字の結合文字対応:日本で人気の家族絵文字(👨👩👧👦)はサロゲートペアで複数コードポイントから成るため、
text.lengthで素朴に数えると見た目と合いません。[...text].lengthを使うと少し改善します。 - 全角英数字と半角英数字の混在:パスワードフィールドやメールアドレス欄では、
autoCapitalize="none"とkeyboardType="email-address"を明示しておかないと、iOSで全角入力モードに入ったまま打ってしまうユーザーが発生します。
次の一歩
まずはお手元のRorkプロジェクトの中から、TextInputを使っている画面をひとつ選んで、onChangeTextに直接APIコールやstate更新の重い処理を書いていないか確認してみてください。もし書いているなら、この記事のデバウンスまたはonSubmitEditingパターンへ差し替えることで、日本語ユーザーの体感品質が目に見えて変わります。
フォーム周辺の実装をさらに磨き込みたい方は、react-hook-formとZodで作るRorkのフォームバリデーション、キーボード表示に関するUI問題はRorkアプリでキーボードが入力フィールドを隠す問題の対処ガイドも参考にしてみてください。日本語ユーザーに『ちゃんと動くアプリだ』と感じてもらえる一歩を、今日から踏み出せます。