コンテンツにスキップ
Webフロントエンドエンジニアのための React Native 実践入門
Esc
navigateopen⌘Jpreview
このページの内容

第6章 React Nativeのコアコンポーネントを知る

View・Text・Image・TextInputがWebのどの要素に当たるのかと、テキストを必ずTextで囲む理由が分かります。

ここからはReact Nativeの基礎パートに入ります。本章ではまず、画面を組み立てるときに最初に出会うコアコンポーネントを整理します。Webの<div><p>に相当するものを正確にマッピングしておくと、以降の章で「あれは何だったか」と立ち止まる回数を減らせます。

4つの基本コンポーネント

最初に押さえておきたいのは次の4つです。

  • View ― レイアウト用のコンテナ。Webの<div>相当
  • Text ― テキストを表示する。Webの<span>/<p>相当
  • Image ― 画像を表示する。Webの<img>相当
  • TextInput ― テキスト入力欄。Webの<input type="text">/<textarea>相当

これだけで実はかなり多くの画面が組めます。

import { View, Text, Image, TextInput, StyleSheet } from "react-native";

function ProfileCard() {
  return (
    <View style={styles.card}>
      <Image source={{ uri: "https://example.com/avatar.png" }} style={styles.avatar} />
      <Text style={styles.name}>Taro</Text>
      <TextInput style={styles.input} placeholder="メッセージを入力" />
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 16, gap: 12 },
  avatar: { width: 64, height: 64, borderRadius: 32 },
  name: { fontSize: 18, fontWeight: "bold" },
  input: { borderWidth: 1, borderColor: "#ddd", borderRadius: 8, padding: 8 },
});

JSXの形はWebと変わらず、要素名とstyleの書き方だけが違います。

なお、これ以降のスニペットでは特記なき限りreact-nativeからのimportを省略します(View/Text/Pressableなど)。手元で写経するときは、上の例のように冒頭で必要なものをまとめてimportしてください。

なぜテキストは必ず <Text> で囲む必要があるのか

Webから来ると最初に戸惑うのが、文字列を<View>に直接置けないことです。

// ✕ エラーになる
<View>こんにちは</View>

// ○ 必ずTextで囲む
<View>
  <Text>こんにちは</Text>
</View>

これは、ネイティブのUIでは「文字を描画する専用の部品」と「レイアウト用の容れ物の部品」がそもそも別物として用意されている、という設計をそのまま反映しているためです(参考: iOSならUILabelUIView、AndroidならTextViewandroid.view.Viewが対応します)。

慣れてしまえば苦になりませんが、最初の壁として知っておくと、エラーメッセージで悩む時間を減らせます。

Textの中にViewを混ぜるときは控えめに

逆方向は完全に禁止というわけではありません。<Text>の中にインラインで小さな<View>(バッジやアイコンなど)を置くケースはあり、対応する場面ではそのまま使えます。ただ、ブロックレベルの<View>を入れ子にしすぎるとプラットフォーム間でレイアウトが崩れやすいので、込み入ったレイアウトはテキストの外に出すのが安全です。

<Text>の中に<Text>を入れて部分的なスタイルを当てるのは何も問題ありません。

<Text style={{ fontSize: 16 }}>
  こんにちは
  <Text style={{ fontWeight: "bold", color: "red" }}>Taro</Text>
  さん
</Text>

Webで<span>を入れ子にするのと同じ感覚です。

iOS/Androidでテキストの見え方が変わる

同じ<Text>に同じスタイルを当てても、iOSとAndroidで「行の高さ」「上下の余白」「ベースラインの位置」がわずかに違って見える、ということがあります。デザインをピクセル単位で揃えようとすると、ここで詰まりがちです。

代表的なのが、Androidで<Text>に自動で付く上下の追加余白(font padding)です。何も指定しないとAndroid側だけ縦に少し膨らんで見えます。これを揃えたい場合はincludeFontPadding: falseを当てます。

<Text style={{ fontSize: 16, includeFontPadding: false }}>こんにちは</Text>

そのほか、固定高さの中で上下中央寄せしたい場合はtextAlignVertical: "center"(Android向け)、行間を揃えたい場合はlineHeightを明示する、といった対処があります。「iOSで合わせたデザインがAndroidで微妙にずれる」と感じたら、まずこの3つ(includeFontPadding / textAlignVertical / lineHeight)を疑うと早いです。

Image ― 画像表示

<Image>は、URLでもローカルアセットでも表示できます。

// リモート画像
<Image
  source={{ uri: "https://example.com/photo.jpg" }}
  style={{ width: 200, height: 200 }}
/>

// ローカル画像(プロジェクト内のファイル)
<Image source={require("./assets/logo.png")} style={{ width: 100, height: 100 }} />

Webと違って、<Image>は表示サイズを明示する必要があります。最低でも幅か高さのどちらか一方を指定しないと、表示領域が0として扱われ画面に何も描画されません。両方を数値で指定するか、片方だけを指定してaspectRatioプロパティで縦横比を補えばOKです(Webでは画像の元サイズが自動で使われましたが、React Nativeでは明示的にレイアウトを決める必要があります)。

resizeModeプロパティでcover/contain/stretch/center/repeatを切り替えられます。Webのobject-fitに相当する役目です。

より高機能なexpo-image

expo-imageを導入すると、

  • 自動キャッシング
  • プレースホルダ画像(blurhashなど)
  • スムーズなフェードイン
  • WebPやAVIFといった効率的なフォーマット対応

といった機能が揃います。本格的にアプリを作るなら、Imageよりexpo-imageを使うのがおすすめです。

npx expo install expo-image
import { Image } from "expo-image";

<Image
  source={{ uri: "https://example.com/photo.jpg" }}
  style={{ width: 200, height: 200 }}
  placeholder={{ blurhash: "L6PZfSi_.AyE_3t7t7R**0o#DgR4" }}
  contentFit="cover"
  transition={300}
/>

contentFitresizeModeに相当し、transitionでフェードイン時間(ms)、placeholderでblurhashなどのプレースホルダを指定できます。

TextInput ― テキスト入力

入力欄はWebの<input>に近い使い方ができます。

import { useState } from "react";
import { TextInput } from "react-native";

function NameInput() {
  const [name, setName] = useState("");

  return (
    <TextInput
      value={name}
      onChangeText={setName}
      placeholder="名前を入力"
      style={{ borderWidth: 1, padding: 8 }}
    />
  );
}

注意点はいくつかあります。

  • 値の変更イベントはonChangeではなくonChangeText(値だけが直接渡ってくる)
  • 複数行にしたい時はmultiline={true}
  • パスワード入力はsecureTextEntry={true}
  • キーボードの種類を切り替えたい時はkeyboardType="email-address"/"numeric"など

<TextInput>もAndroidとiOSで挙動が分かれやすい部品の代表格です。たとえば、

  • 自動補完・自動大文字化のデフォルトが違う(autoCorrectautoCapitalizeを意識的に指定する)
  • 入力欄の高さがプラットフォームのフォント余白の影響を受ける(前述のincludeFontPaddingが関係する)
  • 日本語入力(IME)の確定タイミングや、変換中の文字列の扱いが微妙に異なる

実装時はiOS/Androidの両方で必ず触って、見え方と入力感を確かめるのが安全です。

キーボードの制御(キーボードに隠れない位置にフォーカスを送る、表示中はレイアウトをずらす)はモバイルならではのテーマで、第8章でまとめて扱います。

スクロール領域

ここがWeb開発者にとって最初の大きな分岐点です。

Webでは要素の高さを超えるコンテンツを置けば、自動でスクロールバーが出ました。React Nativeにはそういう振る舞いがありません。スクロールが必要な領域は、明示的に専用のコンポーネントで囲みます

選択肢は主に2つです。

  • ScrollView ― 中身を全部レンダリングしてからスクロールさせる。Webのoverflow: autoに近い
  • FlatList ― リストを仮想化(画面に出ているぶんだけレンダリング)する。大量項目向け

スクロールする内容の性質からScrollViewとFlatListを選ぶ判断図

ScrollView

要素数が少ない、もしくは内容が決まり切っているスクロール領域に使います。

import { ScrollView, Text } from "react-native";

function AboutScreen() {
  return (
    <ScrollView contentContainerStyle={{ padding: 16 }}>
      <Text>長い説明文がここに続きます……</Text>
    </ScrollView>
  );
}

contentContainerStyleにスクロール内側のレイアウトを書きます(styleはスクロールビュー自体のサイズ・背景に効きます)。

FlatList

データの数が増える可能性がある一覧画面では、FlatListを使います。仮想化(画面外のセルをレンダリングしない)が効くため、何千件のデータでもメモリやパフォーマンスを抑えられます。

ScrollViewとFlatListのレンダリング範囲比較

ScrollViewは中身を全部レンダリングするのに対し、FlatListは画面内(viewport)の項目だけをレンダリングし、スクロールに応じて動的に描画・破棄します。件数が増えるほど差が効いてきます。

import { FlatList, Text, View } from "react-native";

type Todo = { id: string; title: string };

function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <FlatList
      data={todos}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 12 }}>
          <Text>{item.title}</Text>
        </View>
      )}
    />
  );
}

主要なpropsの意味は、

  • data ― 配列
  • keyExtractor ― 各項目のkeyを取り出す関数(Reactのkeyと同じ役割)
  • renderItem ― 各項目をどう描画するかを返す関数

「Webのmapで並べる感覚」を、仮想化対応に置き換えたものだと思ってください。

リストを下に引っ張って更新するUXは、Webには無いモバイル定番の動きです。FlatListrefreshControl propに<RefreshControl>を渡すと実装できます。

import { FlatList, RefreshControl, Text, View } from "react-native";
import { useState } from "react";

function TodoList({ todos, onRefresh }: { todos: Todo[]; onRefresh: () => Promise<void> }) {
  const [refreshing, setRefreshing] = useState(false);
  const handleRefresh = async () => {
    setRefreshing(true);
    try {
      await onRefresh();
    } finally {
      setRefreshing(false);
    }
  };

  return (
    <FlatList
      data={todos}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 12 }}>
          <Text>{item.title}</Text>
        </View>
      )}
      refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
    />
  );
}

refreshingは「いま読み込み中か」のフラグで、onRefreshは引っ張ったときに呼ばれる関数です。サーバ再取得だけでなく、ローカルキャッシュの読み直しにも使えます。

行の中身が複雑になってスクロールがカクつく規模になったら、Shopify製の@shopify/flash-listへの置き換えも検討してください。インターフェースはFlatListとほぼ同じで、行の高さ推定をベースに描画コストを抑える作りです。シンプルなリストではFlatListで十分なので、本書ではFlatListを基本にします。

ボタン代わりのコンポーネント

タップ可能な領域を作るには、Webで<button>を使う代わりに、React Nativeでは<Pressable>を使うのが現代の第一選択です。

import { Pressable, Text } from "react-native";

<Pressable
  onPress={() => console.log("tapped")}
  style={({ pressed }) => ({
    opacity: pressed ? 0.6 : 1,
    padding: 12,
  })}
>
  <Text>タップしてください</Text>
</Pressable>

タッチ系コンポーネントは複数あります(TouchableOpacityTouchableHighlightTouchableWithoutFeedbackなど)が、Pressableが後発でこれらを統合しているため、新規実装はPressableを第一選択にしてください。既存コードに残っている分はそのままで問題ありません。詳しくは第8章で扱います。

アクセシビリティの基本

スクリーンリーダー(iOSのVoiceOver、AndroidのTalkBack)対応は、Webのaria-*属性とよく似たプロパティで指定します。最低限押さえておきたいマッピングは次のとおりです。

Web React Native
aria-label accessibilityLabel
role="button" accessibilityRole="button"
aria-disabled accessibilityState={{ disabled: true }}
aria-checked accessibilityState={{ checked: true }}
aria-hidden accessibilityElementsHidden(iOS)/ importantForAccessibility="no-hide-descendants"(Android)

なお、aria-labelaria-hiddenaria-checkedroleといったWeb標準の名前は、React Nativeでもそのまま受け付けます(roleaccessibilityRoleより優先されます)。第1章で「Webの書き方に近づき続けている」と書いたのは、こういう部分です。既存コードや周辺の資料ではaccessibility*の形が主流なので本書もそちらで統一しますが、Webの手癖のまま書いても動く、と知っておくと迷いが減ります。

最低でも、テキストを持たないPressable(アイコンボタン、+ボタンなど)にはaccessibilityLabelを入れます。「+」だけだとVoiceOverが「プラス」と読んでしまうので、目的を表す文言を渡しておきます。

<Pressable
  onPress={handleAdd}
  accessibilityRole="button"
  accessibilityLabel="TODOを追加"
>
  <Text>+</Text>
</Pressable>

ストア審査でもアクセシビリティは見られるので、リリース前に最低限のラベル付けは済ませておきましょう。

本章で押さえたい対応関係

Web React Native
<div> <View>
<span> <p> <Text>
<img> <Image> (本格運用はexpo-image)
<input type="text"> <textarea> <TextInput>
<button> <Pressable>
<a> <Link> (Expo Router)
overflow: auto <ScrollView>
大量リスト <FlatList>

Webのタグの代替を表で持っておくと、新しい画面を組む時の判断が早くなります。

本章のまとめ

  • View / Text / Image / TextInputが基本
  • 文字列は必ずTextで囲む(ネイティブUIの設計思想を反映)
  • 画像はサイズを明示する。本格運用はexpo-image
  • スクロールは明示的に。少量はScrollView、多量はFlatList。引っ張って更新はRefreshControl
  • アクセシビリティはWebのaria-*に対応するaccessibility*プロパティで指定する
  • タップ可能領域はPressableが第一選択

次章では、これらに見た目を与えるスタイリングについて見ていきます。

最終更新 2026年7月24日