ReactエンジニアがReact Nativeを使ってみた

#はじめに
もともとReactを使用してweb開発をしていましたが、React Nativeを使用したモバイル開発に関わることになりました。Reactを使っていた人間がReact Nativeを勉強してみた所感を書きたいと思います。本記事はReact Native公式ドキュメント[[1]]を参考に書いていきます。

#前提
Reactが使える
TypeScriptが使える
モバイルは初めて

環境構築と簡単な動作確認が知りたい人は前回の記事[[1]]で行っているのでそちらを参照してください。

#React Nativeの必要性
現在モバイル端末での使用OSはAndroidOSとiOSで二極化しており、なおかつそれぞれでの使用プログラミング言語も異なります。昔はiOSではObjective-C、AndroidではJavaが使用されていました。しかし、最近ではiOSではSwift、AndroidではKotlinという言語が使われており、2つのOS合わせると最大４つのコードが存在する可能性があるわけです。もちろんこれは開発効率の観点から見て良くなく、これを解決するためにReact Nativeなどのクロスプラットフォームで開発出来るフレームワークが開発されてきたわけです。

#React Nativeでないとだめなのか
近年よく比べられるフレームワークとしてFlutterがあります。よくある議論として「Flutter vs React Native」のようなものがありますが、**開発コストという点で見ればReact Nativeの方がFlutterよりも明らかに効率的です**。FlutterではDartという言語を新しく覚える必要があり、学習コストは高いです。それに対してReact NativeはReactの知識を利用して開発出来るので、学習コストは比較的低いです。パフォーマンス的にはFlutterの方が良いのかもしれませんが、そこまでパフォーマンスを求めないのであれば手軽に作成できるReact Nativeの方が良いでしょう。ただそのまま使えるわけではなく、多少は覚えることがあるので注意です。

ここからはReact Nativeの概念について話していきたいと思います。

#React Nativeのコンポーネント
React Native公式ではコンポーネントを何種類かに分けて呼んでいます。

**Native Components**
それぞれのOSでのViewを担当する部分がバックアップしているコンポーネント。

**Core Components**
React Nativeですぐに使えるNative Componentsのセットコンポーネント。

**community-contributed components**
React Nativeコミュニティが開発する各OS独自のNative Component。

各OSの特定の部分に対応するものがReact Nativeコンポーネントであり、それをまとめたものがCore Componentsであると。恐らく僕たちが使うのは基本的にCore Componentsになるということでしょう。それで足りない場合はcommunity-contributed componentsなどにないか探してみるというような感じなのでしょうか。

公式のベン図的な画像を載せておきます。

![react_native_component.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1545070/c59d9e14-62c9-33e5-576a-36a28a5663bd.png)
[[1]]より引用

あくまでReact Native ComponentsはReact Componentsの部分集合であるということなので、この図からもReact Componentsを理解していれば問題なさそうということが分かりますね。

#React Nativeの文法
さてここからが本題です。ReactにはないReact Native特有の部分を見ていきたいと思います。

###App.tsxを理解する
まずは`expo init`コマンドで作成されたテンプレートの`App.tsx`の解読をしていきましょう。文章はHello,Worldに変更してあります。

```react:App.tsx
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>Hello,World</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});
```
まず、`React`をimportしているようですがこれはReactと違って必要でした。内部的に使っているんでしょうか。`StatusBar`というやつはなんだろうと調べると、モバイル上部に表示される電源残量や電波状況を表すアイコンが並んでいる部分をステータスバーというようです。`View`と`Text`はReact Nativeのコアコンポーネントで、詳しくは後ほど説明します。Viewに対してstyleを適用しているようですが、stylesというオブジェクトのプロパティとしてcontainerを設定し、スタイルを記述しています。スタイルについても後ほど詳しく記述しますが、ここではキャメルケースでの書き方でないといけないようで、Reactでのインラインスタイルによるスタイリングと同じですね。

ざっと見ていきましたが、大枠はReactと変わらないようなので、あとは各コンポーネントを覚えていくだけかなと思います。

###コンポーネントのprops
React Nativeのリファレンスを見ると、propsがiOSとAndroidOSで異なる物が多いようです。また、propsの数自体相当多く、これを全て覚えるのはかなり大変だと思います。この記事では各OS限定のpropsは基本的に扱わず、使いそうなpropsがあれば紹介するという感じで紹介していきますので、詳しく知りたい方は公式のリファレンス[[3]]を参考にしてください。

###View
React Nativeにおける一番基本的なコンポーネントです。divタグのようなものだと思って貰えばいいと思います（厳密には少し違う）。

###SafeAreaView(iOS)
iOSデバイスではノッチや角丸などの物理な違いによって安全にレンダリング出来る範囲が異なります。このコンポーネントでは安全な範囲内でコンテンツをレンダリングすることが出来ます。

###Text
文字を書く場合は基本的にこれを使います。Viewに直接書いても反映されません。

**ネスト出来る**
AndroidとiOSでは文字列の範囲に特定の書式をつけることが出来、この実現のためにTextはネストすることが出来ます。ネストしたテキストだけをスタイリングすることで、任意の範囲に書式をつけることが出来ます。

**・レイアウトが特殊**
また、Textはレイアウトに関して特殊です。Test内の要素は長方形ではなくなる場合があり、行の終わりを見て折り返します。

・スタイルの継承が制限されている
テキストのスタイルはTextによってしか継承されません。つまりViewでテキストに関するスタイルをしても繁栄されません。特定のスタイルのTextを使いまわしたい場合は既存のTextを拡張したコンポーネントを定義することが公式では進められています。Text内では継承されるため、ネストしたTextでは親のスタイルが適用されます。

###Image
画像を利用する際に使います。propsのsourceに参照を渡すことで画像が表示できます。下記に公式の例を載せます。

```react

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

const styles = StyleSheet.create({
  container: {
    paddingTop: 50,
  },
  stretch: {
    width: 50,
    height: 200,
    resizeMode: 'stretch',
  },
});

const DisplayAnImageWithStyle = () => {
  return (
    <View style={styles.container}>
      <Image
        style={styles.stretch}
        source={require('@expo/snack-static/react-native-logo.png')}
      />
    </View>
  );
}

export default DisplayAnImageWithStyle;
```
[[3]]より引用。

###TextInput
入力で使用します。全部説明すると長くなるので例を下記に示します。

```react
import React from "react";
import { SafeAreaView, StyleSheet, TextInput } from "react-native";

const UselessTextInput = () => {
  const [text, onChangeText] = React.useState("Useless Text");
  const [number, onChangeNumber] = React.useState(null);

  return (
    <SafeAreaView>
      <TextInput
        style={styles.input}
        onChangeText={onChangeText}
        value={text}
      />
      <TextInput
        style={styles.input}
        onChangeText={onChangeNumber}
        value={number}
        placeholder="useless placeholder"
        keyboardType="numeric"
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  input: {
    height: 40,
    margin: 12,
    borderWidth: 1,
    padding: 10,
  },
});

export default UselessTextInput;
```
[[3]]より引用

一番ある使い方はonChangeTextにstateのセッターを入れておいて、入力を反映しつつstateで保持みたいな感じだと思います。気になるのはTypeScriptで使う際にnumberとstringどうやって分けているんだろうというとこですね。時間のある時に検証したら更新します。

**・下にボーダーがある**
TextInputはデフォルトでビューの下部にボーダーを持ちます。これはシステムが提供する背景画像によってパディングが設定されており、変更することは出来ません。この問題を回避するには、高さを明示的に設定しないようにして、システムが適切な位置にボーダーを表示するようにするか、underlineColorAndroidをtransparentに設定してボーダーを表示しないようにすることが必要なようです。詳しくは公式リファレンスを見てください。

###ScrollView
リファレンスを直訳したんですが、少し意味がわからないところがあったので分かりにくいかもしれません。

スクロールしたい場合に使います。React NativeではViewを使って表示する場合、表示画面領域より大きいものをレンダリングしようとしても自動的にスクロールするようにはならないようです。スクロールしないことは少ないと思うので基本的にはこれを使うことになるんでしょうか。

**・全ての親Viewが高さ制限されている必要がある**
リファレンスではboundedと書かれていたのですが、この訳で合っているのかあやしいです。高さを制限する方法は２つありViewの高さを直接設定すること（非推奨）と全ての親Viewに高さを設定することです。{flex:1}を忘れるとエラーが出るとリファレンスには書いてあるのですが、よく分かりません。下記に例を載せておきます。

```react
import React from 'react';
import { StyleSheet, Text, SafeAreaView, ScrollView, StatusBar } from 'react-native';

const App = () => {
  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        <Text style={styles.text}>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
          aliquip ex ea commodo consequat. Duis aute irure dolor in
          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
          culpa qui officia deserunt mollit anim id est laborum.
        </Text>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: StatusBar.currentHeight,
  },
  scrollView: {
    backgroundColor: 'pink',
    marginHorizontal: 20,
  },
  text: {
    fontSize: 42,
  },
});

export default App
```
[[3]]より引用。

**showsVerticalScrollIndicator**
このpropsをfalseにすることでスクロールバーを表示しないように出来ます。

**horizontal**
このpropsをtrueにすることで横スクロールにも対応できます。

**FlatlListとの違い**
ScrollViewでは全ての子コンポーネントを一度にレンダリングするため、パフォーマンス上の問題があります。FlatListではアイテムが表示されようとしているときに遅延的にレンダリングし、画面外に大きくスクロールするアイテムを削除することでメモリや処理時間の節約が出来ます。FlatListはほかにもアイテム間の距離、複数列、無限スクロールローディングなどの機能をサポートしているので便利です。

ここまで書いていて思いましたが、もしかして割とScrollViewって要らない子？状況に応じて使い分けるんですかね。

###StyleSheet
ここまででも使用してきていますが、cssのスタイルを使用するために利用しています。特に言うことはないのですが、React NativeはTailwind CSSとは相性が良くなさそうだなーと思います。調べた感じライブラリなどを使えばいけるようですが、vscodeのインテリセンスなどの関係上どうなのかなーという感じです。

逆にCSS in JSは特に変化なく使えそうだなと思います。webとほとんど変わらず記述出来るので、開発コストを削減するという観点から見たら、React NativeではCSS in JSを用いるのがいいんじゃないかなと思います。僕はstyled-componentsを用いて書いているので苦労することはなさそうです。

###Button
ボタンに相当するコンポーネントです。見たほうが早いと思うので下記に例を載せます。

```react
<Button
        title="Press me"
        disabled
        onPress={() => Alert.alert('Cannot press this one')}
      />
```
[[3]]より引用。

propsとしてonPressとtitleは必須のようです。titleでボタンの表示を決めるようですが、childrenでは無理ということでしょうか。Alert.alertでwindow.alertのようなことが出来るようです。またこの例ではdisabledにしているので利用できないような見た目になります。動的に決定したい場合はdisabledのところにboolean型の変数を入れれば可能でしょう。

###Switch
このSwitchはswitch文のことではなく、切り替えるためのボタンのことです（名前あるのかな）。リファレンスのダークモード切り替えの部分によくあるやつです。下記に例を示します。

```react
      <Switch
        trackColor={{ false: "#767577", true: "#81b0ff" }}
        thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
        ios_backgroundColor="#3e3e3e"
        onValueChange={toggleSwitch}
        value={isEnabled}
      />
```
[[3]]より引用
タップすることでboolean型変数がトグルし、valueの値によって色々変わるという感じです。

###TouchableOpacity
押されるとラップされたViewの不透明度が減少し、薄暗くなります。

```react
<TouchableOpacity
        style={styles.button}
        onPress={onPress}
      >
        <Text>Press Here</Text>
      </TouchableOpacity>
```
[[3]]より引用

###FlatList
先程も説明しましたが、リスト表示をする際に利用します。

**data**
表示したい配列データ。

**renderItem**
dataを使ってどのようなものを表示するかを決める関数

**extraData**
追加データ。下の例では再レンダリングするためにstateを渡しています。

**keyExtractor**
デフォルトのkeyプロパティの代わりに使用するidの指定。

**・コンテンツがレンダリング領域の外にいったら内部状態は保持されない**
FlatListの仕様上レンダリングの外のコンテンツの内部状態は保存されません。そのためstateの更新時とレンダリングについて気をつける必要があります

**・コンテンツは画面外で非同期にレンダリングされる**
この特徴によりコンテンツ外のレンダリングよりも早くスクロールすると、一時的に空白のコンテンツが表示される可能性があります。

```react
import React, { useState } from "react";
import { FlatList, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity } from "react-native";

const DATA = [
  {
    id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
    title: "First Item",
  },
  {
    id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
    title: "Second Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e29d72",
    title: "Third Item",
  },
];

const Item = ({ item, onPress, backgroundColor, textColor }) => (
  <TouchableOpacity onPress={onPress} style={[styles.item, backgroundColor]}>
    <Text style={[styles.title, textColor]}>{item.title}</Text>
  </TouchableOpacity>
);

const App = () => {
  const [selectedId, setSelectedId] = useState(null);

  const renderItem = ({ item }) => {
    const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
    const color = item.id === selectedId ? 'white' : 'black';

    return (
      <Item
        item={item}
        onPress={() => setSelectedId(item.id)}
        backgroundColor={{ backgroundColor }}
        textColor={{ color }}
      />
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      <FlatList
        data={DATA}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
        extraData={selectedId}
      />
    </SafeAreaView>
  );
};

・・・
```
[[3]]より引用
map関数によるレンダリングの代わりなのかなーとおもっています。

###Section List
FLatListに見出しがついたような感じのものです。ほとんど同じなので使い方は下のサンプルプログラムを見てください。

```react
import React from "react";
import { StyleSheet, Text, View, SafeAreaView, SectionList, StatusBar } from "react-native";

const DATA = [
  {
    title: "Main dishes",
    data: ["Pizza", "Burger", "Risotto"]
  },
  {
    title: "Sides",
    data: ["French Fries", "Onion Rings", "Fried Shrimps"]
  },
  {
    title: "Drinks",
    data: ["Water", "Coke", "Beer"]
  },
  {
    title: "Desserts",
    data: ["Cheese Cake", "Ice Cream"]
  }
];

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

const App = () => (
  <SafeAreaView style={styles.container}>
    <SectionList
      sections={DATA}
      keyExtractor={(item, index) => item + index}
      renderItem={({ item }) => <Item title={item} />}
      renderSectionHeader={({ section: { title } }) => (
        <Text style={styles.header}>{title}</Text>
      )}
    />
  </SafeAreaView>
);
・・・
```
[[3]]より引用。

#おわりに
ざっと概念と文法を見てきましたが、理解すれば簡単に使えそうです。ただ、想像していたよりは覚えることがあったのであとは実際に開発してから使用感を確かめていきたいと思います。

#参考文献
[1]:https://reactnative.dev/docs/getting-started
[2]:https://qiita.com/it_tsumugi/items/1fde6eaf43df2a4094a9
[3]:https://reactnative.dev/docs/components-and-apis

[[1]]:Introduction
[[2]]:Ubuntu20.04LTSでReact NativeとExpoを試す
[[3]]:Core Components and APIs