本来なら単純なはずの入力フィールドが、思い通りに動かなくて困っていませんか?キーボードが入力欄を隠してしまったり、iOSとAndroidでテキストのレンダリングが異なったり。Controlled componentの値をクリアしても、ユーザーの画面上には残ってしまう。そんな些細なログインフォームの修正が、いつの間にかデバッグ作業に変わってしまうこともあります。
それは、 React NativeのTextInputが持つ性質です。モバイルアプリで最も頻繁に使われるコンポーネントの一つですが、レイアウト、ネイティブキーボードの挙動、バリデーション、アクセシビリティ、そしてプラットフォーム固有のレンダリングが複雑に絡み合っています。多くのチームは、基本的な実装(ハッピーパス)はすぐに習得しますが、ドキュメントにはほとんど記載されていないエッジケースに時間を取られてしまいがちです。
このガイドでは、本番環境でも通用するパターンに焦点を当てます。基本事項はもちろん、QAが両プラットフォームでのテストを開始した後に表面化しがちな、ドキュメント化されていないバグやエッジケースについても解説します。もし、チーム内でモバイル開発の分担についても検討中であれば、 TekRecruiterによるオフショア開発ガイド も併せて読むことをおすすめします。入力処理が多い機能は、実装基準が明確でないと、開発の分担時にトラブルが起きやすい領域だからです。より広範なアーキテクチャの文脈については、こちらの クロスプラットフォーム・モバイルアプリ開発ガイド 手元に置いておく価値があります。
目次
- モバイルフォームの構成要素
- TextInputの基本とクイックリファレンス
- 必須のTextInputパターンと実装例
- Controlled vs Uncontrolled Components の徹底解説
- キーボードとフォーカス制御のマスター術
- アクセシビリティとプラットフォーム間のスタイルの違い
- よくあるバグと高度な解決策
- 統合とエコシステム
モバイルフォームの構成要素
ログイン、サインアップ、検索、決済、プロフィール編集、サポートチケット、管理ツール、医療用入力、現場作業用フォームなど、あらゆるモバイル製品はテキスト入力に依存しています。これらはすべて、同じコアなプリミティブに基づいています。
〜を支えるもの React NativeのTextInput 厄介なのは、コンポーネントツリー上では小さく見えても、非常に多くの責務を負っている点です。Stateとの同期、ネイティブキーボードとの連携、iOSとAndroid間での一貫した挙動、早期のバリデーションフィードバック、そしてアクセシビリティの確保。これらの一つでも欠けると、ユーザーは即座に違和感を覚えます。
なぜこのコンポーネントがこれほどまでに苦痛をもたらすのか
ボタンの不具合は一目でわかりますが、テキストフィールドの不具合はより巧妙で、しばしばそれ以上に深刻です。ユーザーはタップして入力し、アプリが信頼できるものだと信じています。テキストが消えたり、フォーカスが飛んだり、キーボードで入力欄が隠れたりすると、信頼は一気に失墜します。
また、このコンポーネントはネイティブの挙動と密接に関わっています。つまり、バグの原因がReactのStateフロー、スタイリング、プラットフォームのデフォルト設定、あるいはネイティブのイベントハンドリングなど多岐にわたるということです。クリーンなJavaScriptを書いていても、デバイスによって挙動が異なるフィールドが出来上がってしまうことがあります。
実践ルール: 単なるテキスト入力ボックスとしてではなく、一つのUIシステムとして、あらゆる重要な入力フォームを扱いましょう。
プロダクション環境のチームが真に必要としているもの
公式のサンプルは最初のレンダリングまでは助けてくれます。しかし、プロダクションレベルの開発には以下の要素が必要です:
- 予測可能なStateフロー: フィールドは常にアプリケーションのStateを反映している必要があります。
- 信頼できるバリデーションのタイミング: エラーはユーザーを苛立たせるためではなく、助けになるタイミングで表示されるべきです。
- プラットフォームの耐性: iOSとAndroidでは、意図的な調整が必要です。
- デバッグのしやすさ: 問題が発生した際、修正がローカルで完結し、理解しやすいものであるべきです。
だからこそ、優れたチームは早い段階で入力パターンを標準化します。共通のラッパー、バリデーション用propsの命名規則、そして一連のキーボードルールを定めておくことで、後々の予期せぬ混乱を大幅に防げます。
TextInputの基本とクイックリファレンス
ある TextInput 周囲の画面との干渉が始まるまでは、単純に見えます。一つのフィールドが、古い状態(stale state)のトリガー、キーボードの挙動の乱れ、オートフィルの予期せぬ動作、さらにはprops一覧だけでは分からないプラットフォーム固有の挙動を引き起こすことがあります。チームは、早い段階でベースとなる API を標準化することで時間を節約できます。
基本的にはcontrolled inputsを使用すべきですが、そのメリットとコストを理解しておきましょう。controlled fieldを使用すると、レンダリングされる値がReactのstateに紐付くため、バリデーション、リセット、プリフィル、フィールド間のルール適用が予測可能になります。トレードオフとして、すべてのキーストロークがレンダリングパスを通ることになるため、変更のたびに重いフォーマット処理やバリデーションロジックを実行すると、低スペックのAndroid端末でラグが発生する可能性があります。
頻繁に使用するprops
ほとんどのプロダクション環境のフォームにおいて、コアとなるセットアップは依然として value, onChangeText、および placeholderこれら3つで一般的な実装はカバーできますが、周辺のpropsの設定次第で、ネイティブらしい使い心地になるか、ストレスを感じるものになるかが決まります。
実装中やバグの調査時に、私がよく参照しているクイックリファレンスをまとめました。
| Prop | 種類 | 説明 |
|---|---|---|
value |
string | 入力欄に表示されている現在のテキスト。Controlled componentとして扱う場合は、常にコンポーネントの状態と一致させる必要があります。 |
onChangeText |
function | 更新された文字列を受け取ります。特に長いフォームやリスト内で使用する場合は、ハンドラーの処理を軽量に保ってください。 |
placeholder |
string | 値が空のときに表示されるヒントテキスト。これだけに頼らず、ラベルも併用するようにしてください。 |
keyboardType |
string | 次のようなキーボードレイアウトをリクエストします: email-address, number-pad、または phone-pad。実際のレイアウトはプラットフォームによって異なります。 |
secureTextEntry |
boolean | 入力テキストをマスクします。Androidでは、キーボードによって選択や表示の切り替えの挙動が異なる場合があるため、パスワードフィールドのテストを念入りに行う必要があります。 |
autoCapitalize |
string | 大文字・小文字の挙動を制御します。 none メールアドレス、ユーザー名、コードなど、入力内容を正確に保持する必要がある場合に使用してください。 |
maxLength |
number | ネイティブレイヤーで入力文字数を制限します。厳格な制限が必要な場合は、後からトリミングするよりもこちらを使用してください。 |
multiline |
boolean | 複数行の入力を有効にします。これを有効にすると、高さ、垂直方向の配置、および送信時の挙動が変化します。 |
onFocus |
function | フィールドがフォーカスされたときに発火します。touched 状態やアナリティクス、スクロールして表示するロジックに便利です。 |
onBlur |
function | フィールドからフォーカスが外れたときに発火します。遅延バリデーションをトリガーする定番の場所です。 |
returnKeyType |
string | キーボードのアクションラベルを設定します。例: next, done、または search。iOSとAndroidでサポート内容が異なります。 |
onSubmitEditing |
function | キーボードの送信アクションが押されると実行されます。一部の複数行の組み合わせでは、開発者が期待するようには発火しません。 |
placeholderTextColor |
string | プレースホルダーの色を設定します。プラットフォームのデフォルトが異なるため、コントラストは手動で確認してください。 |
editable |
boolean | フィールドのレイアウトを維持したまま、入力を無効にします。無効時のスタイリングは各自で行う必要があります。 |
混乱を招きやすいプロパティがいくつかあります:
keyboardTypeはヒントであり、保証ではありません。デバイスやロケールによっては、数値キーボードでも記号が入力できたり、マイナス記号が省略されたりすることがあります。maxLengthは内部でスライスするよりも安全ですonChangeText。後処理を行うと、制御されたフィールドでカーソルが飛ぶ原因になります。multilineレイアウト以上の変化をもたらします。Androidでは、追加した後にテキストが上揃えにデフォルト設定されることがよくあります。textAlignVertical="top".
最小限の制御された実装例
覚えておくべき基本パターンは以下の通りです:
import React, { useState } from 'react';
import { TextInput, View, StyleSheet } from 'react-native';
export function EmailField() {
const [email, setEmail] = useState('');
return (
<View style={styles.container}>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email address"
keyboardType="email-address"
autoCapitalize="none"
style={styles.input}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
input: {
borderWidth: 1,
borderColor: '#D0D5DD',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
});
これでも動作しますが、実際の code では、通常いくつかの防御的なデフォルト設定を追加します。メールアドレスのようなフィールドでは、 autoCorrect={false} を使用することで、意図せず値が変わってしまうキーボードの自動修正を防げます。複数の入力項目があるフォームでは、refをアタッチして、 returnKeyType="next" を早めに設定しておくことで、後からのフォーカス管理の修正作業を避けられます。入力中に値をフォーマットする場合は、リリース前にカーソルの挙動を必ずテストしてください。制御されたフォーマットは、実機でしか発生しない選択バグを招く最も典型的な原因の一つです。
もう一つ、実用的なルールがあります。バリデーション、送信準備、サーバーのハイドレーション、または条件付きUIに関わるフィールドは、最初から「controlled」として扱ってください。後から「uncontrolled」なフィールドに制御を追加しようとすると、フォーカスの喪失やステートの不一致といったバグが発生しやすくなります。
TextInputの必須パターンと実装例
多くのバグは、一つの汎用的な入力実装を無理に使い回そうとすることで発生します。メールアドレス、パスワード、コメント、フォーマット済み値では、求められるデフォルト設定が異なります。各パターンに最適なpropsを与えてください。
メールアドレスまたはユーザー名入力
import React, { useState } from 'react';
import { TextInput, StyleSheet } from 'react-native';
export function UsernameInput() {
const [username, setUsername] = useState('');
return (
<TextInput
value={username}
onChangeText={setUsername}
placeholder="Username or email"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
returnKeyType="next"
/>
);
}
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: '#CCC',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
});
使用するのは autoCapitalize="none" 認証情報に関連するものにはこれを使用します。キーボードはユーザーを補助するものであるべきで、意図せず値を書き換えてしまうような挙動は避けるべきです。 autoCorrect={false} ユーザー名やメールアドレスには、これもより安全なデフォルト設定です。
パスワード入力
import React, { useState } from 'react';
import { TextInput, View, Pressable, Text, StyleSheet } from 'react-native';
export function PasswordInput() {
const [password, setPassword] = useState('');
const [hidden, setHidden] = useState(true);
return (
<View style={styles.wrapper}>
<TextInput
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry={hidden}
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
returnKeyType="done"
/>
<Pressable onPress={() => setHidden(prev => !prev)} style={styles.toggle}>
<Text>{hidden ? 'Show' : 'Hide'}</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
position: 'relative',
justifyContent: 'center',
},
input: {
borderWidth: 1,
borderColor: '#CCC',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
paddingRight: 60,
},
toggle: {
position: 'absolute',
right: 12,
},
});
ここでの主なトレードオフは、利便性と誤操作による露出のリスクです。表示/非表示の切り替えは入力精度を高めますが、どの画面で有効にするかは慎重に検討すべきです。
複数行のメモやコメント
import React, { useState } from 'react';
import { TextInput, StyleSheet } from 'react-native';
export function NotesInput() {
const [notes, setNotes] = useState('');
return (
<TextInput
value={notes}
onChangeText={setNotes}
placeholder="Add notes"
multiline
textAlignVertical="top"
style={styles.textarea}
/>
);
}
const styles = StyleSheet.create({
textarea: {
borderWidth: 1,
borderColor: '#CCC',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 12,
minHeight: 120,
},
});
textAlignVertical="top" Androidでtextareaのような操作感を実現したい場合は、これが重要になります。これがないと、初期テキストの配置が不自然に感じられることがあります。
フォーマット済み入力とマスキング
電話番号、カード入力、郵便番号、IDなどの入力では、ネイティブの機能は TextInput コンテナとイベントフローは提供しますが、フォーマットのロジックは提供しません。通常、チームはここで小さなフォーマッタを onChangeText 自作するか、マスキングライブラリを導入するかです。
判断基準はシンプルです。フォーマットが軽量かつローカルで完結するなら、自前で実装しましょう。入力にロケール固有のルールやカーソル管理の懸念、複数のマスクパターンがある場合は、専用ライブラリを使ってください。
以下のガードレールを考慮してください:
- レンダー時ではなく、state 内でフォーマットする: 表示される値を決定論的に保ちます。
- カーソル位置を安易にいじらない: カーソルのジャンプは、入力が壊れたと感じさせる最も手っ取り早い原因の一つです。
- バリデーションはフォーマットと分けて行う: 文字列は正しく見えても、ビジネスルールを満たさないことがあります。
最もクリーンな入力コンポーネントは、「ユーザーが入力した値」「表示する値」「バックエンドが期待する値」の3つの責務を分離しています。
Controlled vs Uncontrolled Components の徹底解説
フォームの実装は通常シンプルなものから始まります。しかし、次第にインラインバリデーション、入力値の事前設定、入力が有効になるまでの送信ボタンの無効化、離脱ステップの分析といった要望が増えていきます。Controlled と Uncontrolled のどちらの入力方式を選ぶかで、これらの実装の難易度が大きく変わります。

なぜ Controlled 入力がデフォルトなのか
Controlled なコンポーネントは TextInput React の state で値を保持します。その state を valueに渡し、 onChangeTextで更新します。このメリットは理論上の話ではありません。バリデーション、条件付きレンダリング、送信の準備状態、フィールドのリセット、サーバー主導の更新など、すべてが単一の「信頼できる情報源(source of truth)」に基づいて動作します。
const [email, setEmail] = useState('');
<TextInput
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
このパターンには現実的なトレードオフもあります。キー入力のたびに React の更新が発生するためです。小さなフォームであればそのコストは無視できますが、重い兄弟コンポーネントのレンダリングを伴う大きな画面では、特に低スペックの Android デバイスでタイピングの遅延が目に見えて発生することがあります。もし Controlled なフィールドの動作が重いと感じるなら、問題は通常そのフィールド自体ではなく、 TextInput 周囲のコンポーネントツリーにあります。重い子コンポーネントを memoize し、可能な限りフォームの state をローカルに保ち、パース処理や API の呼び出し、スキーマバリデーションなどを onChangeText.
の内部で直接行わないようにしましょう。Controlled 入力は、state の変化が明示的なためテストも容易です。テストコードでテキストを入力し、レンダリングされた値を検証し、送信を実行してエラーメッセージを確認する際も、ネイティブビューの内部状態を推測する必要がありません。フォーム周りのテストカバレッジを高めたいチームは、 Reactフォームの挙動に関するユニットテスト 後付けではなく、コンポーネント設計の一部として。
非制御入力(uncontrolled inputs)が有効なケース
非制御入力は、現在のテキストをネイティブコンポーネント内に保持し、ref経由または送信時に読み取ります。用途は限定されますが、有効な活用法があります。
主なユースケースは以下の通りです:
- 使い捨ての検索フィールド: 画面側で必要なのは、最終的なクエリやデバウンス後の更新値だけです。
- パフォーマンスが求められる大規模なフォーム: ユーザーが最後に一度だけ送信する場合、すべてのフィールドをReactのstateで管理するのは非効率になる可能性があります。
- サードパーティ製やブリッジ経由のネイティブ入力: 一部のラッパーは、制御されたpropよりも命令的なメソッドをより自然に公開しています。
value。
要件が増えると、すぐにデメリットが目立ち始めます。リアルタイムバリデーションが扱いにくくなったり、送信後のフォームクリアの挙動が不安定になったりします。サーバーからのレスポンスをフィールドに同期させる際、refの書き換えや個別のEffectが乱立しがちです。
開発現場で実際に遭遇するバグ
公式のサンプルでは、controlled inputは非常にシンプルに見えます。しかし、本番環境ではいくつかのエッジケースが次々と発生します。
フォーマット後のカーソル飛び
もし onChangeText キー入力のたびに文字列を書き換えると、カーソルが末尾に飛んだり、予期せぬ動きをしたりすることがあります。電話番号のマスク処理やクレジットカードのフォーマットが主な原因です。対策としては、フォーマットを最小限に抑える、必要な時に選択範囲を保持する、あるいはカーソル状態を正しく扱うマスキングライブラリを使用することが挙げられます。
Androidでの重いレンダリングによる文字落ち controlled fieldへの入力が、親コンポーネントの重い再レンダリング、ネットワークコール、または同期的なバリデーションを引き起こすと発生します。入力が漏れているように見えますが、実際の問題はレンダリングの負荷です。重い処理は入力パスから切り離しましょう。
controlledモードとuncontrolledモードの切り替え
ある時は〜でレンダリングされ、 value and sometimes without it, behavior gets inconsistent fast. Pick one ownership model for the lifetime of the component. If the field is controlled, initialize with '' ではなく undefined または null コンポーネントが明示的にそれらの値を期待していない限りです。
Prefill(事前入力)の競合。
ユーザーが入力し始めた後に非同期データが届くと、よくあるバグが発生します。遅れて届いたサーバーの値が、ローカルの編集内容を上書きしてしまうのです。これを防ぐには、hydrationのパスを保護してください。ユーザーがまだフィールドに触れていない場合のみ取得データを適用するか、フィールドごとにdirty状態を管理するようにします。
実践的なルール
ビジネスロジック、バリデーション、送信状態、またはリモートデータに関連するものは、すべてcontrolled inputsを使用してください。uncontrolled inputsは、アプリが中間的な値を必要とせず、よりシンプルな所有モデルを採用することで明確なメリットが得られる場合にのみ使用してください。
この標準を守ることで、後々の大幅な書き直しを避けることができます。
キーボードとフォーカス制御のマスター
フォームの機能が正しくても、キーボードの挙動が悪いと使いにくく感じられます。ユーザーはすぐにそれに気づきます。キーボードがアクティブなフィールドを隠してしまったり、「次へ」を押しても期待通りに移動しなかったりすると、画面全体の完成度が低く感じられてしまいます。

キーボードによるレイアウト崩れを防ぐ
最初の解決策は構造的なものです。画面の下部にフィールドがある場合は、該当するエリアを KeyboardAvoidingView または、キーボードが入力中のフィールドを覆わないように、keyboard-awareなスクロールコンテナを使用します。
実用的なベースラインは以下の通りです:
import React from 'react';
import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
export function FormScreen({ children }) {
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView keyboardShouldPersistTaps="handled">
{children}
</ScrollView>
</KeyboardAvoidingView>
);
}
ヘッダー、タブバー、または固定フッターがある場合、画面ごとに余白を調整する必要があることがよくあります。一つのラッパーですべてのレイアウトが解決できるとは考えないでください。
意図を持ってフォーカスを移動させる
Refベースのフォーカス管理こそが、複数フィールドのフォームをスムーズに感じさせる鍵です。ステップに合わせて returnKeyType を設定し、次に onSubmitEditing をフォーカスするように紐付けます。
import React, { useRef, useState } from 'react';
import { TextInput, View } from 'react-native';
export function SignupFields() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const passwordRef = useRef<TextInput>(null);
return (
<View>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
returnKeyType="next"
onSubmitEditing={() => passwordRef.current?.focus()}
/>
<TextInput
ref={passwordRef}
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry
returnKeyType="done"
/>
</View>
);
}
また、ここで onFocus と onBlur も役立ちます。多くのチームでは、フォーカス時にボーダーの色を変えたり、blurされるまでエラー表示を遅らせたり、最後のフィールドの送信後にキーボードを閉じたりしています。
チーム内で挙動の標準を合わせるには、視覚的なウォークスルーが役立ちます:
実践的なアドバイス。キーボードを閉じる操作は、フォーカスの移動よりも煩わしく感じられることが多いです。フィールドの外をタップするのは単純に聞こえますが、ScrollViewやボタンとのインタラクションが複雑になることがあります。Storybookのような単体例だけでなく、実際の画面でキーボードの非表示挙動を構築し、テストしてください。
スタイリング、アクセシビリティ、プラットフォーム間の差異
通常、チームはスタイリング、アクセシビリティ、プラットフォームの違いを個別に議論しがちです。しかし、実際のアプリではこれらはつながっています。見た目は洗練されていても、iOSで不自然にテキストが切れたり、スクリーンリーダーに用途が伝わらなかったりする入力フィールドは、完成とは言えません。
長く使い続けられるスタイル
使用するのは StyleSheet.create() 長く使う予定の入力スタイルでは、ボーダー、パディング、角丸、プレースホルダーの色、無効状態、エラー時のバリエーションをチームが一箇所で標準化できます。インラインスタイルは試作には向いていますが、デザインシステムが進化し始めるとすぐに陳腐化します。
安定した入力スタイルには、通常以下が含まれます:
- 一貫したタップ領域: パディングでフィールドをタップしやすくします。
- フォーカス状態とエラー状態の可視化: フィールドがアクティブまたは無効なときは、ユーザーに明確な手がかりが必要です。
- 予測可能なスペーシング: ラベル、ヘルパーテキスト、エラー用のスペースをレイアウトに確保します。
入力欄周りの見た目や視覚的階層を調整しているなら、この React Native linear gradient ガイドは コンテナのスタイリングパターンに関するデザイン寄りの参考資料として役立ちます。
アクセシビリティと iOS 固有の挙動
クロスプラットフォームでの一貫性を保つには、開発者は iOS 特有のレンダリングの癖を考慮する必要があります。例えば lineBreakStrategyIOSこれを設定すると push-out 入力欄は文字列の末尾を省略記号で表示し、Android のデフォルトの挙動と一致します。詳細は React Native のテキスト表示挙動に関するこちらの Stack Overflow スレッド.
で議論されています。また、同じリファレンスでは、入力エリアを KeyboardAvoidingView または KeyboardAwareScrollView で囲むことが、キーボードが入力フィールドを覆う恐れがある場合に不可欠であり、 bottomOffset といった点も指摘しています。 30 異なる画面サイズに合わせてスペーシングを調整するのに役立ちます。また、成熟したチームがデフォルトとして扱うべき2つの基準を強化します。それは、メンテナンス性のための StyleSheet.create() 使用、およびアクセシビリティ向上のための明確なラベル、ヘルパーテキスト、エラーメッセージの提供です。
私がレビュー時に使用している実践的なチェックリストを紹介します:
- すべてのフィールドに明確なラベルを付ける: プレースホルダーテキストは、ラベルの完全な代わりにはなりません。
- ヘルパーテキストとエラーテキストを明示する: ユーザーに何が失敗したのかを推測させてはいけません。
- iOSでの長い値のテスト: テキストの切り捨てや末尾の表示状態は、Androidとは異なる場合があります。
- 画面の向き(Orientation)の変化を確認する: レスポンシブなフォームレイアウトは、予期せぬ形で崩れることがあります。
優れたモバイル入力とは、単にテキストを受け付けるだけではありません。そこに何を入力すべきか、何が間違っていたのか、そして次に何が起こるのかをユーザーに伝えるものです。
よくあるバグと高度な修正方法
時間の大半はこれに奪われます。厄介なのはバグが存在すること自体ではなく、最悪のバグの多くが一見正しく見えるパターンで起きることです。

controlled input のクリアバグ
最も厄介な問題の1つが、controlled input のクリアバグです。state を空文字に設定してフィールドがクリアされることを期待しても、input にフォーカスが残ったまま表示テキストも残ります。
この問題に関するコミュニティの議論では、次のような標準的な方法や clear() 単純な state 更新でも、ネイティブのイベントカウンターをバイパスしてレンダリングの不一致を引き起こす可能性があると示されています。また、同じ議論では、現在唯一信頼できる回避策は key prop ラッパーで強制再レンダリングするか、カスタム forceSetTextAndSelection コマンドを使うことですが、これは公式の API の一部ではありません。また、2024年から2025年のフォーラム議論でも未解決のままで、 50件以上 の Stack Overflow スレッドや Reddit 投稿でこの問題が指摘されています。出典: controlled input のクリアに関する React Native コミュニティの議論.
最小限の回避策は以下の通りです:
import React, { useState } from 'react';
import { TextInput, View, Button } from 'react-native';
export function ClearableField() {
const [value, setValue] = useState('');
const [inputKey, setInputKey] = useState(0);
const clearField = () => {
setValue('');
setInputKey(prev => prev + 1);
};
return (
<View>
<TextInput
key={inputKey}
value={value}
onChangeText={setValue}
placeholder="Type something"
/>
<Button title="Clear" onPress={clearField} />
</View>
);
}
洗練された方法ではありませんが、確実です。
iOSにおけるテキスト消失問題
もう一つのカテゴリは、iOSでの表示の退行です。長い値の入力や特定のスタイル設定の組み合わせにより、入力後にテキストのレンダリングが止まったり、消えたりすることがあります。
修正方法は、デバッグ作業よりも単純な場合が多いです:
- 追加
flex: 1レイアウトが必要な箇所: flex制約の不足がレンダリングを壊すことがあります。 - 以下の項目を
selectionpropを注意深く確認してください: 誤った使い方が表示上の問題を引き起こす可能性があります。 - 戦略的にmemoizationを試してください: 親コンポーネントのレンダリングを安定させることで、表示の乱れを抑えられます。
- 使用するのは
multiline={true}ただし、フィールドの挙動と一致する場合に限ります: パッチとして機能しますが、盲目的に導入しないでください。
本番環境のデバッグでこれらの問題を早期に発見したい場合は、 モバイルおよびJSアプリ向けのアプリヘルスモニタリング UIリグレッションのフィードバックループを短縮するのに役立ちます。
大量の入力項目が再レンダリングされる際のパフォーマンス
入力に関するパフォーマンスのアドバイスは、しばしば教条的になりがちですが、実際はもっと単純です。すべてのフィールドを事前に最適化する必要はありません。入力状態によって、兄弟コンポーネントの重いレンダリング、フォーマット処理、または繰り返されるバリデーションロジックが発生する画面を最適化してください。
有効な戦術としては、状態を各フィールドの近くに配置する、親画面の再レンダリングが多い場合はフィールドのラッパーをmemo化する、重いバリデーションや検索によるサイドエフェクトをdebounceするといった方法があります。よくある罠は、早すぎる段階ですべてをグローバルステートに押し込み、タイピングが重くなった原因が分からなくなることです。
タイピングに遅延を感じる場合は、入力自体を疑う前に、キー入力のたびに他に何が再レンダリングされているかを確認してください。
統合とより広範なエコシステム
TextInputが単体で使われることは稀です。プロダクション環境のアプリでは、フォームライブラリ、分析用フック、バリデーション層、API クライアント、そしてデザインシステムの中に組み込まれています。チームが実装の一貫性を保てることこそが重要であり、そのためのエコシステムが不可欠です。
フォームライブラリとTextInputの併用
FormikとReact Hook FormはどちらもネイティブのTextInputと相性が良いですが、チームに異なる習慣をもたらします。制御された状態(controlled state)パターンを好むチームには、明示的で馴染みのあるFormikが適しています。一方、React Hook Formはボイラープレートを削減し、フォームが大規模になった際の再レンダリングのオーバーヘッドを抑えることができます。
リアルタイムバリデーションでは、ユーザー体験を損なわないことが重要です。入力中は高速かつローカルにバリデーションを行い、重いチェックはblur(フォーカス外れ)や送信時に行うようにしましょう。メールアドレスやユーザー名、IDにはRegexベースのルールが一般的ですが、チームでそれらのパターンをレビューする際には、 Digital ToolPadのRegexガイド リリース前に正規表現をテストするための実用的なリソースとして役立ちます。
ネイティブのTextInputだけで十分な場合
多くのアプリでは、ネイティブのTextInputだけで十分です。特に、ラベル、ヘルパーテキスト、エラー状態、フォーカス時のスタイリングを備えた小さなラッパーコンポーネントをチームで管理している場合はなおさらです。このアプローチは、早い段階で重いUIライブラリを導入するよりも、多くの場合で優れた結果をもたらします。
サードパーティのコンポーネントライブラリは、フル機能のデザインシステム、一貫したテーマ設定、そして多くの画面にわたるフォームのプリミティブが必要な場合に有効です。ただし、トレードオフとして抽象化が伴います。開発スピードは上がりますが、エッジケースのデバッグ時にライブラリ特有の挙動に悩まされることになります。
ラッパーの設計判断に影響するため、iOSに関する重要な注意点をここに記しておきます。もう一つの見落とされがちな問題は、iOSにおけるテキストの視認性の低下です。特に長い値や特定のスタイリングを適用した場合、入力したテキストが消えてしまうことがあります。議論の要約は TextInputに入力したテキストが表示されない件に関するこちらのStack Overflowのスレッド にまとめられており、 flex: 1 や不適切な selection propの使用といった一般的な原因が指摘されています。コミュニティによる修正策としては、入力を useMemo でラップするか、 multiline={true}を設定する方法があります。これらは有用なパッチですが、デフォルトのアーキテクチャにするべきではありません。
より広範なモバイルスタックを比較検討しているチームや、ネイティブコンポーネントの挙動がWeb指向のアプローチと乖離し始めたチームにとって、 React NativeとCapacitorの比較 は有用なリファレンスとなるでしょう。
実用的な結論はシンプルです。まずはネイティブのTextInputと、規律あるラッパーから始めてください。デザインシステムや開発スピードの観点から、追加の抽象化が必要だと判断できた段階でライブラリへ移行しましょう。
もし、Webベースのスタックでモバイルアプリをリリースしており、ストアの審査を待たずにJavaScript、CSS、文言、設定、アセットの修正をより安全に配信する方法を求めているなら、 Capgo チェックする価値があります。チームに制御された Live Update、ロールアウトチャネル、ロールバック保護、リリース可視性を提供するため、フォームや入力フローのUIの問題を迅速に修正する必要がある場合に特に重宝します。