本文書は、Reactの基礎知識を持っており、React Nativeを利用してモバイルアプリを作成してみたいと思っているけれどどのように行えばいいのかわかないというReact Native初心者向けの内容になっています。React Nativeでのモバイルアプリ開発の基礎知識を学ぶことでことで、Reactアプリ開発との違いなども理解することができます。

React Nativeの動作確認にはmacOSを利用しています。

2024年7月に最新情報にリライトしています。
fukidashi

React Nativeとは

React NativeはJavaScriptライブラリのReactを利用してiOS, Andoroid向けのアプリを開発することでできるクロスプラットフォームモバイルアプリケーションフレームワークです。iOSのアプリを作成するためにSwift, Objective-C, Androidのアプリを開発するためにJava, Kotlinといった言語を習得する必要がなくJavaScriptのみでiOS, Android上で動作するアプリを開発することができます。iOS用、Android用に異なるコードで記述する必要がなくJavaScriptの1つのコードベースで複数のプラットフォームで動作させることができます。

Reactをベースに開発されていることからReactで記述するコードと共通箇所が多々あるためReactの知識があれば比較的簡単にReact Nativeを使ってモバイルアプリを開発することができます。大きな違いとしてReactではブラウザ上に描写するためにdivタグ, pタグなどのhtmlタグを利用しますが React NativeではViewタグ, TextタグなどのReact Native側であらかじめ準備されているコンポーネントを利用します。

クロスプラットフォームのアプリを開発する際にReact Nativeと比較されるフレークワークにFlutterがあります。大きな違いはプログラミング言語です。React NativeはJavaScript, FlutterはDartを利用します。またReact NativeはFacebook, FlutterはGoogleによって開発が行われています。

動作確認を行なったmacOSのバージョンはsonoma14.5, Xcodeのバージョンは15.3です。

環境の構築

React Nativeを開発する方法にはExpo ToolとReact Native CLIの2つのツールがあります。Expo Toolを利用せずに開発を行うことができますが現在はExpoで開発を行うのが主流です。

Expoサイトのトップページ
Expoサイトのトップページ

Nodeのインストールの確認

Reactと同様にReact Nativeを利用するためにはExpoをインストールする環境にNodeがインストールされている必要があります。要件は現行のNode.jsのLTSなのでnode -vコマンドで手元の環境のNodeのバージョンを確認してください。


 % node -v
v20.15.0

Nodeのバージョンが現行のLTSよりも低い場合はNodeのアップデートやNodeのインストールが完了していない場合はインストールを行なってください。

プロジェクトの作成

Expoを利用してReact Nativeのアプリケーションを開発する際に最初にプロジェクトの作成を行います。プロジェクトの作成は”npx create-expo-app@latest”コマンドで行うことができます。コマンドにはオプション–templateを設定することでJavaScript, TypeScriptの選択や最小限のライブラリのみインストールされた状態のBlankなどを選択することができます。–templateオプションを設定しない場合にはdefaultが選択されExpo CLI, Expo Router, TypeScriptなど推奨構成でインストールが行われます。本文書ではblankを選択します。任意の名前のプロジェクト名をつけることができるのでここでは”react-native-first”という名前を設定しています。


 % npx create-expo-app@latest --template
✔ Choose a template: › Blank
✔ What is your app named? … react-native-first
✔ Downloaded and extracted project files.
> npm install
//略
✅ Your project is ready!

To run your project, navigate to the directory and run one of the following npm commands.

- cd react-native-first
- npm run android
- npm run ios
- npm run web

プロジェクトの作成が完了したら、プロジェクトディレクトリreact-native-firstに移動してpackage.jsonファイルの中身を確認しておきます。expoのバージョンが51.0.18であることが確認できます。


{
  "name": "react-native-first",
  "version": "1.0.0",
  "main": "expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "expo": "~51.0.18",
    "expo-status-bar": "~1.12.1",
    "react": "18.2.0",
    "react-native": "0.74.3"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0"
  },
  "private": true
}

開発サーバの起動

package.jsonファイルのscriptを見るとstart, android, iosなどのコマンドが登録されています。開発サーバを起動するために”npm start”コマンドを実行します。

% python3 main.py t,j,p,h,c,pyのいずれかを入力してください。t

% npm start

> react-native-first@1.0.0 start
> expo start

Starting project at /Users/mac/Desktop/react-native-first
Starting Metro Bundler

//ここにQRコード表示

› Metro waiting on exp://192.168.2.200:8081
› Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

› Using Expo Go
› Press s │ switch to development build

› Press a │ open Android
› Press i │ open iOS simulator
› Press w │ open web

› Press j │ open debugger
› Press r │ reload app
› Press m │ toggle menu
› Press o │ open project code in your editor

› Press ? │ show all commands

Logs for your project will appear below. Press Ctrl+C to exit.

npm startコマンドを実行して下記のエラーが発生した場合はHomebrewでwatchmanをインストールする(% brew install watchman)ことでエラーは解消されました。


Error: EMFILE: too many open files, watch
    at FSEvent.FSWatcher._handle.onchange (node:internal/fs/watchers:207:21)

開発中のアプリの画面を確認するためには手元のiPhoneまたはAndroidの携帯に”Expo Go”のアプリをインストールするかiOSのsimulator(macOSの場合)を利用する必要があります。”Expo Go”のインストールはスムーズに進めることができます。

“Expo Go”についてはこちらのページから確認できます。iOSのsimulatorの場合はXcodeのインストールが必要になります。

Xcodeをインストールしても実際にiOSのsimulatorを動作させるためにXcodeのsettingのPlatformsにiOSのインストールを行うことで動作させることができました。

XcodeのSettingからPlatformの確認
XcodeのSettingからPlatformの確認

iOS simulatorが起動すると下記のような画面が表示されます。

iOSシュミレーターで確認
iOSシュミレーターで確認

Expo Goからのアクセス

手元にiPhone, iPadがあり同じネットワークに接続されている場合はExpo Goを利用して開発中のアプリケーションの動作確認を行うことができます。

まずiPhoneもしくはiPadでApple Storeを開いてExpo Goをインストールしてください。

expo clientのインストール
expo goのインストール

インストールが完了したら”npm start”コマンドを実行した時に表示されているQRコードをiPhoneのカメラで撮影してください。カメラがQRコードを認識したら”Expo Goで開く”のメッセージが表示されるのでクリックするとExpo Goが起動してReact Nativeのアプリ画面が表示されます。

React Nativeの基本動作確認

シュミレーター画面またはiPhoneに表示されているApp.jsファイルの内容を確認します。ViewタグとTextタグを確認することができます。divタグやpタグのようなhtmlのタグではなくreact-nativeからimportされているコンポーネントです。


import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Viewコンポーネントはdivタグのように要素をグループ化するために利用することができます。内側のコンテンツにスタイルを適用するなどdivタグのように利用することができます。Textコンポーネントは文字列を表示させるために利用します。HTMLではタグをつけなくても文字列は表示されますがTextコンポーネントを利用せず文字列だけ記述するとエラーになります(Text strings must be rendered whithin as <TEXT> component)。

Viewコンポーネントを利用することで 各プラットフォームの描写(UIView, div, android.view)を意識することなく開発することができます。Viewコンポーネントを利用することで開発したアプリのViewコンポーネントの部分をWebブラウザで確認するとdivに変換されています。
fukidashi

Textコンポーネント

Text内の文字列を更新して保存するとExpo GoまたはiOS simulatorにも即座に更新した内容が反映されます。


export default function App() {
  return (
    <View style={styles.container}>
      <Text>Hello World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

スタイルの適用

Viewコンポーネントのにstyleを適用したい場合はpropsのstyleを利用します。App.jsファイルのViewコンポーネントのstyleに設定されているstyles.containerの中身を見ることでどのようなstyleが適用されているか確認することができます。StyleSheetをreact-nativeからimportしてcreateすることでstyle.containerを設定しています。

containerはcssのclassのように見えますがオブジェクトを設定し、各プロパティはキャメルケースで設定を行う必要があります。ViewコンポーネントのFlexboxのflex-directionのデフォルトはcolumnのためflex:1を設定することで画面いっぱいの領域を取り、alignItemsとjustifyContentで中央に表示させています。


const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

alignItemsとjustifyContentを削除すると文字は左上の表示されます。上部にはノッチもありノッチに被ると文字が見えなくなります。

中央表示を解除
中央表示を解除

Viewコンポーネントの代わりにSafeAreaViewコンポーネントを使うことでこ文字が隠れないように表示されます。


import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, SafeAreaView } from 'react-native';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <Text>Hello World</Text>
      <StatusBar style="auto" />
    </SafeAreaView>
  );
}
SafeAreaViewコンポーネント
SafeAreaViewコンポーネント

Textコンポーネントにstyleを適用したい場合はViewコンポーネントと同様にpropsのstyleを設定することで行うことができます。styles.textを新たに追加することで太文字の赤いのHello Worldが表示されます。


import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Hello World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: 'red',
    fontSize: 'bold',
  },
});

直接(インライン)コンポーネントにstyleを適用したい場合はReactと同様にカーリブレースの中にオブジェクトを使って設定することができます。


export default function App() {
  return (
    <View style={styles.container}>
      <Text style={{ color: 'red', fontWeight: 'bold' }}>Hello World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

Tailwind CSSの利用

React NativeでもTailwind CSSを利用することができます。React NativeでTailwind CSSを利用するためのパッケージがいくつかあるようですが本書ではTailwind React Native ClassnamesNativeWindをインストールします。

Tailwind React Native Classnames

twrncパッケージのインストールを行います。


% npm install twrnc

インストール後にtwrncからtwをimportし、propsのstyleの中にtwと入力しバッククォートの中にTailwind CSSのutility Classを記述するとスタイルが適用されます。


import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import tw from 'twrnc';

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={tw`text-gray-500 font-bold`}>Hello World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

NativeWind

NativeWindの場合は依存関係があるので3つのライブラリをインストールします。NativeWindの方が設定に手間がかかりますがclassName属性を利用してutility classの設定を行うことができます。


% npm install nativewind@^4.0.1 react-native-reanimated tailwindcss

インストールが完了したらTailwind CSSの設定ファイルtailwind.config.jsファイルを作成します。


 % npx tailwindcss init

Created Tailwind CSS config file: tailwind.config.js

tailwind.config.jsファイルには以下のコードを記述します。contentにはTailwind CSSを利用したいファイルへのパスを設定します。


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./app/**/*.{js,jsx,ts,tsx}', './App.js'],
  presets: [require('nativewind/preset')],
  theme: {
    extend: {},
  },
  plugins: [],
};

babel.config.jsファイルを更新します。


module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
      'nativewind/babel',
    ],
  };
};

metro.config.jsファイルをプロジェクトディレクトリ直下に作成します。


const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');

const config = getDefaultConfig(__dirname);

module.exports = withNativeWind(config, { input: './global.css' });

プロジェクトディレクトリ直下にglobal.cssファイルを作成します。


@tailwind base;
@tailwind components;
@tailwind utilities;

App.jsファイルで作成したglobal.cssファイルをimportします。


import './global.css';
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { Text, View } from 'react-native';

export default function App() {
  return (
    <View className="flex-1 bg-white justify-center items-center">
      <Text className="text-xl text-red-700 font-bold">Hello World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

これで設定は完了です。うまく動作しない場合には”npm start”コマンドを再実行してください。

Tailwind CSSを普段から使っている人にとってはstyleの適用もTailwind CSSを利用することで開発効率も上がります。

コンポーネントを追加

React Nativeでのコンポーネントの追加方法を確認します。コンポーネントの追加についてはReactと同じ方法で行うことができます。

compotensフォルダを作成しHeader.jsファイルを作成します。コンポーネントで記述するテンプレートはエディターにVisual Studio Codeを利用している場合はES7 React/Redux/GraphQL/React-Native snippetsの拡張機能をインストールしてHeader.jsファイルで”rnfe”と打ってください。ファイル名を元に下記のテンプレートが表示されるので時間を短縮してコンポーネントのコードの記述が可能です。


  import { View, Text } from 'react-native'
  
  const Header = () => {
    return (
      <View>
        <Text></Text>
      </View>
    )
  }
  
  export default Header

Headerコンポーネントにはtitle(タイトル)をpropsで受け取れるように設定を行います。


import { View, Text, StyleSheet } from 'react-native';

const Header = ({ title }) => {
  return (
    <View style={styles.header}>
      <Text style={styles.text}>{title}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  header: {
    height: 90,
    backgroundColor: 'lightblue',
  },
  text: {
    marginTop: 50,
    color: 'white',
    fontSize: 20,
    textAlign: 'center',
  },
});

export default Header;

App.jsでは作成したHeaderコンポーネントをimportしてpropsでユーザ一覧という文字列を渡します。


import { StyleSheet, View } from 'react-native';
import Header from './components/Header';

export default function App() {
  return (
    <View style={styles.container}>
      <Header title="ユーザ一覧" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

iOS simulatorの上部にユーザ一覧という文字列と背景色が表示されます。

ヘッダーの表示
ヘッダーの表示

App.jsでViewではなくSafeAreaViewコンポーネントを利用した場合は下記のようになります。SafeAreaViewの部分には背景が適用されないようになります。

SafeViewAreaを利用した場合
SafeViewAreaを利用した場合

ユーザ一覧の取得(useState, useEffect)

通常モバイルアプリを作成する際、モバイル上の保存されているデータを利用するのではなく外部リソースから情報を取得してアプリ上に表示することになります。

ここではユーザ一覧を外部リソースから取得して画面上に表示させるためReact HookのuseState, useEffectを利用します。ReactのHookについてはReactと同様の方法で利用することができます。

外部リソースには無料で利用できるJSONPlaceHolderを利用します。https://jsonplaceholder.typicode.com/usersにアクセスすることで10名分のユーザ情報を取得することができます。データの取得にはfetch関数を利用します。

コンポーネントのマウント時にJSONPlaceHolderからデータを取得できるようにuseEffect Hookを使います。


import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import Header from './components/Header';

export default function App() {
  useEffect(() => {
    const getUser = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/users');
      const users = await res.json();
      console.log(users);
    };
    getUser();
  }, []);

  return (
    <View style={styles.container}>
      <Header title="ユーザ一覧" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

App.jsにuseEffectの処理を追加して保存すると”npm start”を実行したターミナル上にユーザ情報が表示されます。

ユーザ情報を外部リソースから取得できることが確認できたらuseState Hookで取得したデータをusersに保存します。保存したデータをmap関数を利用して展開しています。


import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import Header from './components/Header';

export default function App() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const getUser = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/users');
      const users = await res.json();
      setUsers(users);
    };
    getUser();
  }, []);

  return (
    <View style={styles.container}>
      <Header title="ユーザ一覧" />
      {users.map((user) => (
        <Text key={user.id}>{user.name}</Text>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

画面上には10名分のユーザ情報が表示されます。

mapでusersを展開
mapでusersを展開

確認した通り、map関数でusersを展開してユーザ一覧を表示させることもできますがReact Nativeでリスト化する際にFlatListコンポーネントを利用することができます。

FlatListコンポーネントによるリスト化

FlatListコンポーネントではdata, renderItem, keyExtractorの3つのpropsを設定します。dataには取得したusers情報を設定します。renderItemではdataに渡したusersの個別のデータを描写するために関数を指定します。keyExtractorには一意となる値を設定します。通常はidなどを設定します。


import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, FlatList } from 'react-native';
import Header from './components/Header';

//略
<View style={styles.container}>
  <Header title="ユーザ一覧" />
  <FlatList
    data={users}
    renderItem={({ item }) => (
      <View>
        <Text>{item.name}</Text>
      </View>
    )}
    keyExtractor={(item) => item.id}
  />
</View>

FlatListコンポーネントを利用するとmap関数と同じように表示されます。

mapでusersを展開
FlatListsでユーザ一覧を表示

renderItemに直接描写処理を記述していましが関数として分けることもできます。


const renderItem = ({ item }) => (
  <View>
    <Text>{item.name}</Text>
  </View>
);

return (
  <View style={styles.container}>
    <Header title="ユーザ一覧" />
    <FlatList
      data={users}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
    />
  </View>
);

styleを設定することで表示を変更することができます。リストの間をあけるためpaddingの設定を行なっています。


import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, FlatList } from 'react-native';
import Header from './components/Header';

const renderItem = ({ item }) => (
  <View style={styles.item}>
    <Text>{item.name}</Text>
  </View>
);

export default function App() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const getUser = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/users');
      const users = await res.json();
      setUsers(users);
    };
    getUser();
  }, []);

  return (
    <View style={styles.container}>
      <Header title="ユーザ一覧" />
      <FlatList
        data={users}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  item: {
    padding: 40,
  },
});
paddingの設定
paddingの設定

paddingを開けることで画面上で10名分すべてのユーザ情報を一つの画面上で閲覧することができなくなりましたがヘッダーは固定のままリスト部分のみスクロールすることができます。ユーザの先頭はLeanne Grahamでしたがスクロールを行い、下記の画像ではClementine Bauchが上に表示されています。スクロールしてもユーザ一覧のヘッダーはそのままなことが確認できます。

スクロール
スクロール

リスト間に区切りをつけたといった場合にはFlatListコンポーネントのItemSeparatorComponentを設定することができます。ItemSeparatorComponentには関数を設定することができ、Viewコンポーネントにstyleを設定することで区切りの線を入れています。


return (
  <View style={styles.container}>
    <Header title="ユーザ一覧" />
    <FlatList
      data={users}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      ItemSeparatorComponent={() => (
        <View
          style={{
            backgroundColor: 'lightgray',
            height: 1,
          }}
        ></View>
      )}
    />
  </View>
);
リスト間に区切りを表示
リスト間に区切りを表示

FlatListによるデータのリスト表示について理解することができました。

TouchableOpacityの設定

モバイルアプリの場合は指を画面にタッチすることでさまざまな操作を行うことができます。要素をタッチすることができるようにTouchableOpacityというコンポーネントが存在します。TouchableOpacityにはonPressイベントを設定することができるのでリストをタッチすると指定した処理を実行することができます。

Buttonコンポーネントも存在しますがButtonコンポーネントを利用すると表示に各OSが持つ元々のボタン要素の影響をうけるためTouchableOpacity, Pressableコンポーネントを利用します。
fukidashi

TouchableOpacityコンポーネント設定後にリストをタッチするとタッチしたリストだけ不透明度が下がり、薄暗くなります。TouchableOpacityを利用するためにreact-nativeからのimportが必要です。


import React, { useState, useEffect } from 'react';
import {
  StyleSheet,
  View,
  Text,
  FlatList,
  TouchableOpacity,
} from 'react-native';
import Header from './components/Header';

const renderItem = ({ item }) => (
  <TouchableOpacity>
    <View style={styles.item}>
      <Text>{item.name}</Text>
    </View>
  </TouchableOpacity>
);
//略

背景が白だとわかりにくいのでstyleに背景色を設定します。


//略
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  item: {
    padding: 40,
    backgroundColor: '#88cb7f',
  },
});

2つ目のリストをタッチした時は下記のようになりどのリストをタッチしているかがわかります。

touchableopacityの変化
touchableopacityの変化

onPressイベントを利用することで指定した動作を行うことができるのか確認するために画面にアラートを表示させます。Alertコンポーネントを利用してitemのnameを表示させています。Alertを利用するためにreact-nativeからのimportが必要です。


import React, { useState, useEffect } from 'react';
import {
  StyleSheet,
  View,
  Text,
  FlatList,
  TouchableOpacity,
  Alert,
} from 'react-native';
import Header from './components/Header';

const renderItem = ({ item }) => (
  <TouchableOpacity
    onPress={() => {
      Alert.alert(item.name);
    }}
  >
    <View style={styles.item}>
      <Text>{item.name}</Text>
    </View>
  </TouchableOpacity>
);
//略

リストをタッチするとAlertが画面上に表示されます。onPressイベントを利用することで別の処理を行うことができることが確認できました。この後onPressイベントを利用してリストの削除などを実装していきます。

Alertの表示
Alertの表示

TouchableOpacityコンポーネント以外にPressableコンポーネントもあります。PressableコンポーネントのほうがTouchableOpacityコンポーネントよりも他の機能を備えています。ここでのケースではTochableOpacityからPressableに変更した場合にAlertは動作しますがタッチした時のリストの変化はなくなります。Pressableコンポーネントについての詳細は説明しないのでドキュメントを確認してください。

画像の表示

画面上に画像を表示したい場合はImageコンポーネントを利用することができます。画像はpravatar.ccというサイトから取得します。https://i.pravatar.cc/150にアクセスするとランダムでアバター画像が取得できます。

リストの名前の横にアバター画像を表示させます。Imageコンポーネントではpropsのsourceで画像のURLを設定しstyleで画像のheight, widthを設定します。リストで画像と名前を横並びにするためflex-directionをrowに設定しています。


import React, { useState, useEffect } from 'react';
import {
  StyleSheet,
  View,
  Text,
  FlatList,
  Alert,
  Pressable,
  Image,
} from 'react-native';
import Header from './components/Header';
//略
const renderItem = ({ item }) => (
  <Pressable
    onPress={() => {
      Alert.alert(item.name);
    }}
  >
    <View style={styles.item}>
      <Image
        source={{ uri: 'https://i.pravatar.cc/150' }}
        style={styles.avatar}
      />
      <Text>{item.name}</Text>
    </View>
  </Pressable>
);
//略

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  item: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 40,
    backgroundColor: '#88cb7f',
  },
  avatar: {
    height: 50,
    width: 50,
    borderRadius: 30,
    marginRight: 8,
  },
});
画像の表示
画像の表示

Imageコンポーネントを利用して画像を表示することができました。

https://i.pravatar.cc/150から取得する画像はランダムなためリロードを行うと異なる画像が表示されます。
fukidashi

アイコンの設定

アプリケーションを構築する際に頻繁にアイコンを利用する機会があるので設定方法を確認しておきます。expoを利用している場合にはexpo/vector-iconsを利用することができます。

expo/vector-iconsはデフォルトでインストールされているので追加のライブラリのインストールは必要ありません。icons.expo.fyi.からアイコンを検索することができ、サイトで検索を行い探していたアイコンをクリックするとimport方法と設定方法が表示されるのでそれに従って設定を行います。FontAwesome、AntDesign、Featherなどさまざまなセットを利用するためimportの記述方法が異なります。

コードではユーザ一覧の文字とアイコンを横並びにするたflex-directionをrowに設定しています


import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Feather } from '@expo/vector-icons';

const Header = ({ title }) => {
  return (
    <View style={styles.header}>
      <View style={styles.title}>
        <Feather name="users" size={24} color="black" />
        <Text style={styles.text}>{title}</Text>
      </View>
    </View>
  );
};
const styles = StyleSheet.create({
  header: {
    height: 90,
    backgroundColor: 'lightblue',
  },
  title: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    marginTop: 50,
  },
  text: {
    fontSize: 20,
    marginLeft: 8,
  },
});

export default Header;

ユーザ一覧の文字列の左側にユーザのアイコンが表示されました。

アイコン設定
アイコン設定

そのほかにreact-native-vector-iconsライブラリを利用することでもアイコンの設定を行うことができます。

expo/vector-iconsはreact-native-vector-iconsを利用しているので設定できるアイコンは同じです。
fukidashi

% npm install --save react-native-vector-icons

react-native-vector-iconsでもセット毎に同じ名前のアイコンがあるためimportする際にどのセットを利用するか指定します。今回はFeatherのusersを利用しています。

どのようなアイコンがあるかはhttps://oblador.github.io/react-native-vector-icons/から検索して確認することができます。

react-native-vector-icons/FontAwesomeからIconコンポーネントをimportしてnameに利用するアイコンの名前を指定しています。FontAwesomeとは異なる設定のアイコンを利用したい場合はFeatherをFontAwesome等に変更すると別のアイコンになります。


import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Feather';

const Header = ({ title }) => {
  return (
    <View style={styles.header}>
      <View style={styles.title}>
        <Icon name="users" size={25} />
        <Text style={styles.text}>{title}</Text>
      </View>
    </View>
  );
};

expo/vector-iconsを利用した時と表示は変わりません。

フッターの設定

上部のヘッダーのみHeaderコンポーネントで設定を行なっていたので画面の下部にフッターを設定する場合はどうように表示させるか確認します。フッター用のコンポーネントのためにcomponentsフォルダにFooter.jsファイルを作成します。高さと背景とFooterという文字を入れたシンプルなコンポーネントです。


import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

const Footer = () => {
  return (
    <View style={styles.footer}>
      <Text>Footer</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  footer: {
    height: 90,
    backgroundColor: 'lightblue',
  },
});

export default Footer;

App.jsファイルではFlatListコンポーネントの下にFooterコンポーネントを追加します。


//略
import Header from './components/Header';
import Footer from './components/Footer';

export default function App() {
//略

  return (
    <View style={styles.container}>
      <Header title="ユーザ一覧" />
      <FlatList
        data={users}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
        ItemSeparatorComponent={() => (
          <View
            style={{
              backgroundColor: 'lightgray',
              height: 1,
            }}
          ></View>
        )}
      />
      <Footer />
    </View>
  );
}

//略

設定したフッターがどのように表示されるか確認します。フッターは最下部に表示され真ん中のFlatListの部分はスクロールを行うことができます。スクロールしてもヘッダーとフッターの位置が変わることはありません。ヘッダーとフッターの固定を簡単に行うことができました。

フッターの追加
フッターの追加

フッター表示の理解を深めるためにstyleのcontainerに設定しているflexの1を削除するとフッターは表示されなくなります。flexが1という設定がポイントであることがわかります。

FlatListではなくmap関数を利用した場合ではどのようになるか確認してみましょう。


<View style={styles.container}>
  <Header title="ユーザ一覧" />
    {users.map((user) => (
      <Text key={user.id} style={styles.item}>
        {user.name}
      </Text>
    ))}
  <Footer />
</View>

flexは1ですがフッターが画面に表示されることはなくリスト部分をスクロールすることができません。ヘッダーとフッターを上下に固定するためにはflexの設定以外にも他の影響を受けていることがわかります。map関数の展開部分をScrollViewコンポーネントで包みます。


<View style={styles.container}>
  <Header title="ユーザ一覧" />
  <ScrollView>
    {users.map((user) => (
      <Text key={user.id} style={styles.item}>
        {user.name}
      </Text>
    ))}
  </ScrollView>
  <Footer />
</View>

//略
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  item: {
    padding: 40,
    backgroundColor: '#88cb7f',
  },
});

ScrollViewを設定することでリストがスクロール可能となりフッターが表示されFlatListと同じ状態になります。しかしmapの場合はリストの区切りを設定していないためリストの区切りとなるボーダーは表示されません。

リストのpadding40から10に小さくしてリストが画面一杯に広がらない場合を確認してみると下記のようにフッターとScrollViewの間に空白が表示されることになります。

リストが全面に広がっていない場合
リストが全面に広がっていない場合

ScrollViewを利用しない場合も確認するとFooterが一番したではなくmap関数の下に表示されます。

ScrollViewがない場合
ScrollViewがない場合

この動作確認からヘッダーとフッターの固定にはflexとスクロールが可能の要素が関連していることがわかりました。

FlatListとScrollViewによって同様の処理が行えることがわかりましたが違いを確認しておきます。FlatListはリストの遅延読み込みを行い、ScrollViewの場合は一括でリストを読み込むためパフォーマンスに違いがあるようです。ほかにもFlatListでは本書でも確認したように区切り(separator)を設定したり、マルチコラムやinfiniteスクロールなどの機能を利用することができます。

FlasListもScrollViewもリストが縦に並んでいますがもし水平にスクロールさせたい場合はFlatListではpropsのhorizontalをtrueに設定することで可能です。ScrollViewについてはタグの中にhorizontalを追加してください。
fukidashi

リストの削除

ユーザからの操作を受け付けるため表示しているユーザ一覧のリストからユーザを削除するための削除機能の追加を行います。削除の実装を通してpropsを利用して関数を渡す方法も一緒に学びます。

App.jsファイルでdeleteUser関数を追加します。


//略
const deleteUser = (id) => {
  setUsers((prevUsers) => {
    return prevUsers.filter(prevUser.id !== id);
  });
};
//略

追加したdeleteUserをrenderItemに渡せるように新たにコンポーネントのListUser.jsファイルをcomponentsフォルダの中に作成します。FlatListコンポーネントのrenderItem Propsの中でListUserコンポーネントを利用します。その際ListUserコンポーネントにはpropsでitemとdeleteUserを渡します。


import React, { useState, useEffect } from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import Header from './components/Header';
import Footer from './components/Footer';
import ListUser from './components/ListUser';

export default function App() {
  const [users, setUsers] = useState([]);

  const deleteUser = (id) => {
    setUsers((prevUsers) => {
      return prevUsers.filter((prevUser) => prevUser.id !== id);
    });
  };

  useEffect(() => {
    const getUser = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/users');
      const users = await res.json();
      setUsers(users);
    };
    getUser();
  }, []);

  return (
    <View style={styles.container}>
      <Header title="ユーザ一覧" />
      <FlatList
        data={users}
        renderItem={({ item }) => (
          <ListUser item={item} deleteUser={deleteUser} />
        )}
        keyExtractor={(item) => item.id}
        ItemSeparatorComponent={() => (
          <View
            style={{
              backgroundColor: 'lightgray',
              height: 1,
            }}
          ></View>
        )}
      />
      <Footer />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

ListUser.jsファイルには下記を記述します。削除が行えうかどうかを識別するためのゴミ箱アイコンをAntDesignから利用しています。IconコンポーネントにはonPressイベントを設定し、ゴミ箱をタッチするとpropsで受け取ったdeleteUser関数が実行されユーザ一覧からユーザが削除されます。


import React from 'react';
import { StyleSheet, View, Text, Alert, Pressable, Image } from 'react-native';
import Icon from 'react-native-vector-icons/AntDesign';

const listUser = ({ item, deleteUser }) => (
  <View style={styles.row}>
    <View style={styles.item}>
      <Image
        source={{ uri: 'https://i.pravatar.cc/150' }}
        style={styles.avatar}
      />
      <Text>{item.name}</Text>
    </View>
    <Icon name="delete" size={20} onPress={() => deleteUser(item.id)} />
  </View>
);

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 40,
    backgroundColor: '#88cb7f',
  },
  item: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  avatar: {
    height: 50,
    width: 50,
    borderRadius: 30,
    marginRight: 8,
  },
});

export default listUser;
ゴミ箱アイコンの表示
ゴミ箱アイコンの表示

Erin Howellの右側のゴミ箱を削除するとその行が削除されることが確認できます。

削除後のユーザ一覧
削除後のユーザ一覧

ユーザの追加(TextInput)

ユーザの追加を行うためにTextInputコンポーネントを利用します。TextInputコンポーネントの追加を行う前にAppコンポーネントにaddUser関数を追加します。

ユーザを追加する際に一意のIDが必要なのでuuidライブラリを利用します。


% npm install uuid

React Nativeではuuidを利用すると”getRandomValues() not supported”のエラーが発生するのでreact-native-get-random-valuesも一緒にインストールします。


% npm install react-native-get-random-values

addUser関数は名前を受け取り、現在のusersの配列の先頭に新たにユーザを追加します。


import React, { useState, useEffect } from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import Header from './components/Header';
import Footer from './components/Footer';
import ListUser from './components/ListUser';
import AddUser from './components/AddUser';
import 'react-native-get-random-values';
import { v4 as uuidv4 } from 'uuid';

export default function App() {
  const [users, setUsers] = useState([]);

  const addUser = (name) => {
    setUsers((prevUsers) => {
      return [{ id: uuidv4(), name }, ...prevUsers];
    });
  };

  //略

名前を入力するためのコンポーネントファイルAddUser.jsファイルをcomponentsフォルダに追加します。

TextInputに入力した文字を保存するためにuseStateを利用します。文字を入力するとonChangeTextイベントが発火sれsetUserで文字がuserに設定されます。入力した文字が表示されるか確認します。


import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TextInput,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';

const AddUser = ({ addUser }) => {
  const [user, setUser] = useState('');

  return (
    <View>
      <TextInput
        style={styles.input}
        onChangeText={setUser}
        value={user}
        placeholder="ユーザ名を入力してください"
      />
      <Text>{user}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  input: {
    height: 40,
    padding: 8,
    margin: 5,
  },
});

export default AddUser;

入力した文字列が表示できること確認できました。TextInputコンポーネントによって表示された要素にJohn Doeを入力するとその下にJohn Doeが表示されます。

入力した文字の表示
入力した文字の表示

文字を表示した後にボタンをクリックするとユーザ一覧に名前が追加できるようにボタンを追加します。ボタンはTouchableOpacityコンポーネントを利用しています。


<View>
  <TextInput
    style={styles.input}
    onChangeText={setUser}
    value={user}
    placeholder="ユーザ名を入力してください"
  />
  <TouchableOpacity style={styles.btn} onPress={handleUser}>
    <View style={styles.text}>
      <Icon name="plus" size={20} />
      <Text>ユーザ追加</Text>
    </View>
  </TouchableOpacity>
</View>

btn, textのstyleを追加します。


const styles = StyleSheet.create({
  input: {
    height: 40,
    padding: 8,
    margin: 5,
  },
  btn: {
    backgroundColor: 'lightblue',
    paddingVertical: 10,
    marginBottom: 10,
  },
  text: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

ボタンを押した際に実行されるonPressに設定されているhandleUser関数を追加します。handleUserの中で親コンポーネントであるApp.jsファイルから渡されたaddUser関数に引数userを設定しています。addUser関数実行後はinput要素を空にするためsetUserを実行しています。


const handleUser = () => {
  addUser(user);
  setUser('');
};

実際にユーザ名を入力してユーザ追加ボタンをタッチしてください。ユーザ追加ボタンをタッチするとリストの上部に入力した名前が表示されることを確認してください。

ユーザを追加
ユーザを追加

リストへのユーザの追加とリストからのユーザの削除を実装することができました。

今回の場合はinput要素が上部にあるため問題はありませんがinput要素が下部にある場合入力しようとしても表示されるキーボードに文字が隠れて入力できない場合や入力後のボタンが見えないので押せない場合があります。そのような場合はViewコンポーネントの代わりにKeyboardAvoidingViewコンポーネントを利用することで問題を解決することができます。propsのbehaviorにpaddingを設定します。


<KeyboardAvoidingView
  behavior="padding"
  style={{
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1,
  }}
>

本書を読み終えた人はReact Nativeで画面間の移動を実現するために利用することができるReact Navigationについて読み進めることをお勧めしています。