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

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

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と比較されるフレークワークにFlutterがあります。大きな違いはプログラミング言語です。React NativeはJavaScript, FlutterはDartを利用します。またReact NativeはFacebook, FlutterはGoogleによって開発されています。

環境の構築

React Nativeを開発する方法にはExpoとReact Native CLIの2つのツールがあります。本文書では初心者でも容易に開発を進めることができるExpoを利用して動作確認を行います。

Expo
Expo

Nodeのインストールの確認

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


 % node -v
v16.13.0

もしNodeのインストールを行っていない場合はインストールしてください。

Expo CLIのインストール

Nodeのインストールが確認できたら、npmコマンドを利用してExpo CLIのインストールを行います。


 % npm install -g expo-cli

プロジェクトの作成

React Nativeプロジェクトを作成するためにexpo initコマンドを実行します。実行するとtemplateを選択する画面が表示されます。一番上のblankを選択してください。expo initの後に任意の名前のプロジェクト名を指定します。ここではreact-native-firstという名前にしています。複数の画面間の移動を行うために利用するreact-navigationが設定された状態でインストールしたい場合は”tabs”を選択してください。


 % expo init react-native-first
? Choose a template: › - Use arrow-keys. Return to submit.
    ----- Managed workflow -----
❯   blank               a minimal app as clean as an empty canvas
    blank (TypeScript)  same as blank but with TypeScript configuration
    tabs (TypeScript)   several example screens and tabs using react-navigation and TypeScript
    ----- Bare workflow -----
    minimal             bare and minimal, just the essentials to get you started

✔ Choose a template: › blank               a minimal app as clean as an empty canvas
✔ Downloaded and extracted project files.
📦 Using npm to install packages.
⠏ Installing JavaScript dependencies.

✅ 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 start # you can open iOS, Android, or web from here, or run them directly with the commands below.
- npm run android
- npm run ios
- npm run web

インストールが完了したらプロジェクトフォルダに移動してください。


 % cd react-native-first

開発サーバを起動するためにnpm start(expo start)コマンドを実行してください。Expo Developer Toolsの画面が表示されます。

Expo Developer Tool
Expo Developer Tool

左側にシュミレーターを起動するためのメニューが表示されています。最初にRun in web browserをクリックしてください。ブラウザが起動し下記の画面が表示されます。

ブラウザ起動
ブラウザ起動

画面はシンプルですが、表示されている内容はプロジェクトフォルダの直下にあるApp.jsに記述されています。


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>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',
  },
});

Web上で表示することができましたがReact Nativeはクロスプラットフォームでアプリを開発できるため同じ内容でiOSやAndroid OSでも表示させることができます。

iOSのシュミレーターを起動するためにExpo Developer Toolの画面の左側のメニューに表示されている”Run on iOS simulator”をクリックします。画面の左側にINFOバーが表示されてAttempting to open a simulatorが表示され続けている場合はnpm startコマンドを実行したコンソールを確認してください。Xcodeがインストールされていない場合はインストールする必要があります。

画面右下にメッセージが表示
画面右下にメッセージが表示

Xcodeをインストールする必要があると表示されているのでメッセージが表示されている場合は”Y”を押してください。


? Xcode needs to be installed (don't worry, you won't have to use it), would you like to continue to the App Store? › (Y/n)

Xcodeのダウンロード画面が表示されます。Xcodeのダウンロードを行うので雲のアイコンをクリックしてください。ダウンロードが開始されます。

Xcodeダウンロード画面
Xcodeダウンロード画面

Xcodeのダウンロードが完了したら”開く”ボタンをクリックしてください。

ダウンロード完了
ダウンロード完了

ライセンス承諾の画面が表示されるので”Agree”ボタンをクリックしてください。

ライセンス承諾
ライセンス承諾

インストールが開始され、終了するとWelcome to Xcode画面が表示されます。

Welcome to Xcode
Welcome to Xcode

コンソールには”Going to the App Store, re-run Expo when Xcode is finished installing.”(Xcodeのインストールが完了したらExpoを再実行してください。)が表示されるのでCtl+ Cで一度停止再度npm startを実行してください。

Expo Develpment Toolが起動するので”Run on iOS simulator”をクリックしてください。

Expo Developer Tool
Expo Developer Tool

npm startコマンドを実行したターミナルにはExpo Goのインストールのメッセージが表示されるのでインストールにしばらく時間がかかります。


Installing the Expo Go app on iPhone 8 [================================================================] 100% 0.0s
Installing Expo Go 2.19.6 on iPhone 8

インストールが完了するとiOSのシュミレーターの画面に’Open in “Expo Go”?’のメッセージが表示されるので”Open”ボタンをクリックしてください。

iOSエミュレーターの起動
iOSエミュレーターの起動

“Open”をクリックすると次の画面が表示されます。”Got It”ボタンをクリックしてください。

画面
画面

Reloadなどが表示されたメニューが表示されます。

メニューが表示
メニューが表示

一番上のReloadボタンまたは右上のXボタンをクリックするとApp.jsファイルに記述されていた内容が表示されます。

App.jsの内容が表示
App.jsの内容が表示

先ほどのメニューは”Ctl + d” + “Cmd + d”ボタンを押すと再表示されます。

iOSのエミューレーターの起動が確認できたのでアプリケーションを構築するための環境構築は完了です。

iPhone, iPadからのアクセス

手元にiPhone, iPadがあり同じネットワークに接続されている場合はExpo Goを利用してMacで起動している開発中のReact Nativeアプリケーションにアクセスすることが可能です。

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

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

インストールが完了したらExpo Develpment Toolの左下に表示されるQRコードを利用してアクセスすることが可能です。

Expo Developer Tool
Expo Developer Tool

カメラを利用してQRコードを経由して開発中のアプリにアクセスするとExpo Goが起動してiOSのシュミレーターで表示されている同様の画面がiPhone上にも表示されます。手元のiPhoneを開発に利用することができます。

React Nativeの基本動作確認

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


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>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コンポーネントを利用せず文字列だけ記述するとエラーになります。

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

Textコンポーネント

Text内の文字列を更新して保存するとシュミレーターにも即座に更新した内容が反映されます。


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

スタイルの適用

Viewコンポーネントのにstyleを適用したい場合はpropsのstyleを利用します。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 Classnamesをインストールします。


% 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-red-500 font-bold`}>Hello World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

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 React from 'react'
  import { View, Text } from 'react-native'
  
  const Header = () => {
    return (
      <View>
        <Text></Text>
      </View>
    )
  }
  
  export default Header

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


import React from 'react';
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 React from 'react';
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,
  },
});

シュミレーターの上部にユーザ一覧という文字列と背景色が表示されます。

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

Viewコンポーネントと以外にもSafeAreaViewコンポーネントというものが存在します。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の処理を追加して保存するとexpoを起動したコンソールにユーザ情報が表示されます。

ユーザ情報を外部リソースから取得できることが確認できたら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>
  );
}

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

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

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

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

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


<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の設定を行います。


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

return (
  <View style={styles.container}>
    <Header title="ユーザ一覧" />
    <FlatList
//略
    />
  </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コンポーネントを利用します。

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


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が必要です。


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から取得する画像はランダムなためリロードを行うと異なる画像が表示されます。

アイコンの設定

アプリケーションを構築する際に頻繁にアイコンを利用する機会があるので設定方法を確認しておきます。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を利用しているので設定できるアイコンは同じです。

% 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を追加してください。

リストの削除

ユーザからの操作を受け付けるため表示しているユーザ一覧のリストからユーザを削除するための削除機能の追加を行います。削除の実装を通して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について読み進めることをお勧めしています。