第8章 React Nativeのユーザー入力
タップにPressableを使う理由から入力欄がキーボードに隠れる問題への対処まで、モバイルの2大操作をひととおり実装できます。
モバイルアプリでのユーザー入力は、「タップ」と「テキスト入力」の2つが大半を占めます。本章では、それぞれをReact Nativeでどう扱うかを見ていきます。
タップ系コンポーネント
Webの<button>に相当するものとして、React Nativeにはタッチ可能なコンポーネントがいくつかあります。歴史的経緯で複数存在していますが、新規実装ではPressableを第一選択にしてください。
Pressable
最も柔軟で、推奨される選択肢です。
import { Pressable, Text } from "react-native";
<Pressable
onPress={() => console.log("tapped")}
style={({ pressed }) => ({
opacity: pressed ? 0.6 : 1,
backgroundColor: "#007aff",
padding: 12,
borderRadius: 8,
})}
>
<Text style={{ color: "#fff", textAlign: "center" }}>送信</Text>
</Pressable>
styleにオブジェクトではなく関数を渡すと、pressed(押下中かどうか)、hovered(マウスホバー時。Web対応用)、focused(フォーカス時)の状態に応じてスタイルを変えられます。
押下中の見た目を変えたい時は、ほぼpressedを見れば足ります。
Pressableのその他のイベント
onPress以外にも、
onPressIn― 押し始めた瞬間onPressOut― 指を離した瞬間onLongPress― 長押しdelayLongPress― 長押しと判定するまでの時間(ms)
など、状況に応じて使い分けられます。
Touchable系の現在地
過去にはTouchableOpacity、TouchableHighlight、TouchableWithoutFeedback、TouchableNativeFeedbackといったコンポーネントが使われていました。今でも動きますが、Pressableがこれらの機能を統合しているため、新規実装で選ぶ理由はほとんどありません。
既存のコードベースに残っている分はそのままでも構いませんが、新しい画面はPressableで揃えていくのがおすすめです。
ボタン代わりのラッパーコンポーネント
毎回<Pressable>にスタイルを書くのは冗長なので、共通のボタンコンポーネントを作っておくと便利です。
import { Pressable, Text } from "react-native";
type ButtonProps = {
label: string;
onPress: () => void;
variant?: "primary" | "secondary";
};
export function Button({ label, onPress, variant = "primary" }: ButtonProps) {
return (
<Pressable
onPress={onPress}
style={({ pressed }) => ({
opacity: pressed ? 0.6 : 1,
backgroundColor: variant === "primary" ? "#007aff" : "#eee",
padding: 12,
borderRadius: 8,
})}
>
<Text style={{ color: variant === "primary" ? "#fff" : "#333", textAlign: "center" }}>
{label}
</Text>
</Pressable>
);
}
イベントハンドラはpropsで受け取り、内部の関数名はhandleプレフィックスにする ― といった命名は、Web開発と同じ感覚で問題ありません。
ジェスチャーは別ライブラリで
「スワイプで削除」「ピンチで拡大」「ドラッグで並び替え」のような複雑なジェスチャーは、PressableではなくReact Native Gesture Handlerという別ライブラリで実装します。
第12章でアニメーションと一緒に扱うので、ここでは「タップ以上の操作はGesture Handlerに任せる」という方針だけ覚えておいてください。
キーボードの扱い
ここからがモバイル特有の話です。Web開発では「キーボードの存在」を意識する場面はほぼありませんが、モバイルでは入力欄をタップした瞬間に画面の下半分以上をキーボードが覆います。これに対する備えが必要です。
よくある問題
入力欄を画面下のほうに置いていると、キーボードが立ち上がった時に入力欄が隠れてしまいます。Webならscroll-into-viewが自動で効いたり、CSSで対応できたりしますが、React Nativeでは明示的に対処します。

標準のKeyboardAvoidingView
React Nativeに標準で入っている対応はKeyboardAvoidingViewです。キーボードの出現に応じて、自分の高さやpaddingを調整してくれます。
import { KeyboardAvoidingView, Platform, TextInput } from "react-native";
function Form() {
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TextInput placeholder="入力" />
</KeyboardAvoidingView>
);
}
behaviorプロパティに何を指定するかでiOSとAndroidの挙動が変わります。iOSはpadding、Androidはheightまたはundefinedが扱いやすい、というのが経験則です。
ただし、KeyboardAvoidingViewはあくまで標準提供の最小限の機能で、複雑な画面構成だと「うまくいかない」「位置がカクつく」ことが少なくありません。
react-native-keyboard-controller
より滑らかで安定したキーボード制御を求める場合は、サードパーティのreact-native-keyboard-controllerを導入するのが現代の定番です。
npx expo install react-native-keyboard-controller
このライブラリの強みは、
- ネイティブ側でアニメーションを処理するため、滑らかでカクつかない
- iOSとAndroidの挙動を揃えやすい
- 入力欄の上部に「次へ/完了」のツールバーを簡単に出せる
- キーボード出現時に特定要素を自動でスクロールしてフォーカスに合わせられる
といった点です。本格的にフォームを作るアプリでは、最初からreact-native-keyboard-controllerを入れてしまうのがおすすめです。
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
function Form() {
return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior="padding">
<TextInput placeholder="入力" />
</KeyboardAvoidingView>
);
}
APIは標準のKeyboardAvoidingViewと似ているので、置き換えは比較的スムーズに行えます。
キーボードを閉じる
「画面の余白をタップしたらキーボードを閉じたい」という挙動は、Webでは自動でしたがモバイルでは明示的に閉じる必要があります。
import { Keyboard, Pressable } from "react-native";
<Pressable onPress={Keyboard.dismiss} style={{ flex: 1 }}>
{/* 中身 */}
</Pressable>
Keyboard.dismiss()を呼べばキーボードが閉じます。タップ時の見た目を変えたくないので、ここではPressableにpressedスタイルは指定していません。
画面全体をラップしてキーボードを閉じる用途では、伝統的にTouchableWithoutFeedbackが使われてきました。既存コードで見かけても驚かないでください。react-native-keyboard-controllerを使っている場合は、ライブラリ側にもキーボードを閉じる仕組みが用意されています。
フォームのバリデーション
フォームのバリデーションや状態管理は、Web側で使い慣れているライブラリ(React Hook Form、TanStack Form、Valibotなど)がそのまま使えます。React Nativeに固有の事情はほとんどありません。
import { useForm, Controller } from "react-hook-form";
import { TextInput, Text, View } from "react-native";
function NameForm() {
const { control, handleSubmit, formState } = useForm({ defaultValues: { name: "" } });
return (
<View>
<Controller
control={control}
name="name"
rules={{ required: "名前は必須です" }}
render={({ field: { value, onChange } }) => (
<TextInput value={value} onChangeText={onChange} placeholder="名前" />
)}
/>
{formState.errors.name && <Text>{formState.errors.name.message}</Text>}
</View>
);
}
ポイントは、HTMLの<input>がそのまま動くわけではないので、Controllerコンポーネントを介して<TextInput>と接続することです。
本章のまとめ
- タップ系はPressableを第一選択。Touchable系は既存コード互換のために残っている
- 共通ボタンコンポーネントを作って繰り返し使うとコードがすっきりする
- キーボード制御は標準の
KeyboardAvoidingViewより、react-native-keyboard-controllerが現代の定番 - フォームライブラリはWeb側のものがほぼそのまま使える(
<TextInput>をControllerで包む)
次章では、画面と画面をつなぐ仕組み ― ナビゲーションを扱います。Expo Routerによるファイルベースルーティングが主役です。