ReScriptでreason-react-navigationのTabNavigationを動かす

# 要約

ReasonMLやReScriptは参考資料が少なく、実装で困った時の対応が難しい。
今回は、ReScript x Reason React Nativeの環境下で、reason-react-navigationのライブラリの仕様を追いつつ、TabNavigationを動かしてみた。


# reason-react-navigationを利用した既存実装

[reason-react-navigationのexample](https://github.com/reason-react-native/reason-react-navigation/blob/master/src/Example.re)では`StackNavigator`を用いてmodal画面を出す実装が書かれている。

▼[reason-react-naigation-example](https://github.com/reason-react-native/reason-react-navigation-example)では`DrawerNavigator`とTabを組み合わせている。これを基準にすればそれなりのアプリはできそうである。

![reason-react-naigation-example](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/7ea21d16-95fd-0624-fcba-3d3439234503.png)

今回は`TabNavigator`を利用して、アプリの骨組みを作る。

# エディタ設定

ReasonML/ReScript環境向けのプラグインがあるので、入れておく。
[ReScript Docs / Language Manual / EditorPlugins](https://rescript-lang.org/docs/manual/latest/editor-plugins)

# 初期設定

[Reason React Nativeの公式ドキュメント "Create a new project with Reason React Native"](https://reason-react-native.github.io/en/docs/install/)に従い、コマンドを実行する。

```bash
npx @react-native-community/cli init --template @reason-react-native/template <your-project-name>
```

するとプロジェクトに必要なファイル一式が作成される。
プロジェクトのディレクトリに移り、npmのパッケージ群をインストールする。

```bash
cd <your-project-name>
yarn insyall
```

.re/.resファイルをbuild、あるいはwatchする。そして、通常のreact-native同様、run-ios/run-androidを実行する。
コマンドは`package.json`の`scripts`の項で確認できる。

```json:package.json(summarized)
"scripts": {
  "start": "react-native start",
  "ios": "react-native run-ios",
  "android": "react-native run-android",
  "re:clean": "bsb -clean-world",
  "re:watch": "bsb -clean-world -make-world -w",
  "re:build": "bsb -clean-world -make-world",
  "test": "jest"
}
```

```bash
yarn re:watch
```

でbuildを行い、.bs.js形式ファイルを出力し、ファイルの変更があった場合に自動で再出力する。

別窓で`react-native`を走らせる。iOSの場合は`pod install`が必要。

```bash:(iosの場合)
yarn ios
```

# .re -> .res

この手順は行わなくても良い。ただ、App.reは削除する必要がある。

デフォルトのテンプレートはReasonMLで書かれているが、今回はReScriptで記述するので、変換してみる。  
ReasonMLのファイル(.re)からReScriptのファイル(.res)はコマンド一つで変換する事ができる。

```bash
node_modules/.bin/bsc -format src/App.re > App.res
```

元のファイルが残るため、このまま起動するとバグるので注意。古いファイルは消す。

動かすと`Reason React Native`のReasonMLの際と同じ画面が見られる。

![Reason React Native Default](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/6bcd97e8-af45-5127-793d-e11ea36577d2.png)


# 目標構造
[Javascript実装のサンプル](https://reactnavigation.org/docs/tab-based-navigation)の2例を組み合わせると以下のようになっている。
これを元にReScriptを書いていく。

```jsx
import * as React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

// You can import Ionicons from @expo/vector-icons/Ionicons if you use Expo or
// react-native-vector-icons/Ionicons otherwise.
import Ionicons from 'react-native-vector-icons/Ionicons';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === 'Home') {
              iconName = focused
                ? 'ios-information-circle'
                : 'ios-information-circle-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'ios-list-box' : 'ios-list';
            }

            // You can return any component that you like here!
            return <Ionicons name={iconName} size={size} color={color} />;
          },
        })}
        tabBarOptions={{
          activeTintColor: 'tomato',
          inactiveTintColor: 'gray',
        }}
      >
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Settings" component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}
```

# Tab実装

まず、単純なTab Navigationを実装する。

## package

`reason-react-navigation`とそれに該当するライブラリをインストールする

```bash
yarn add @react-navigation/bottom-tabs @react-navigation/native react-native-screens react-native-safe-area-context reason-react-navigation
```

`bsconfig.json`に`reason-react-navigation`を追加する

```json:bsconfig.json(bs-dependencies)
"bs-dependencies": [
  "reason-react",
  "reason-react-native",
  "reason-react-navigation"
]
```

## ディレクトリ

```
src
├── App.res
├── RootNavigator.res
└── screens
    ├── Home.res
    ├── Second.res
    └── Third.res
```

```reasonml:Home.res
open ReactNative

@react.component
let make = (~navigation, ~route) => <SafeAreaView><Text> {React.string("home")} </Text></SafeAreaView>
```
`Home.res`, `Second.res`, `Third.res`は同様に作成、ファイル名と文字列だけ変更したものを用意する。

```reasonml:RootNavigator.res
open ReactNavigation

module RootNavigator = {
    include BottomTabs.Make({
        type params = unit;
    })

    @react.component
    let make = () => <>
        <Native.NavigationContainer>
            <Navigator>
                <Screen name="main" component=Home.make />
                <Screen name="second" component=Second.make />
                <Screen name="third" component=Third.make />
            </Navigator>
        </Native.NavigationContainer>
    </>
}

```

```reasonml:App.res
open RootNavigator

@react.component
let app = () => <RootNavigator />
```

# カスタマイズ

## tabBarOptions

`Tab.Navigation`の`tabBarOptions`を記述する。型で指定されているので、オブジェクトを入れても通らない。

`Tab.Navigator`のPropsは

```reasonml:reason-react-navigation/src/BottomTabs.re(navigatorProps)
type navigatorProps = {
  initialRouteName: option(string),
  screenOptions: option(optionsCallback),
  backBehavior: option(string),
  _lazy: option(bool),
  tabBar: option(Js.t(bottomTabBarProps) => React.element),
  tabBarOptions: option(bottomTabBarOptions),
};
```

と定義されていて、`tabBarOptions`が該当する。
続いて、型として指定されている`bottomTabBarOptions`を調べる。

```reasonml:reason-react-navigation/src/BottomTabs.re(bottomTabBarOptions)
[@bs.obj]
external bottomTabBarOptions:
  (
    ~keyboardHidesTabBar: bool=?,
    ~activeTintColor: string=?,
    ~inactiveTintColor: string=?,
    ~activeBackgroundColor: string=?,
    ~inactiveBackgroundColor: string=?,
    ~allowFontScaling: bool=?,
    ~showLabel: bool=?,
    ~showIcon: bool=?,
    ~labelStyle: ReactNative.Style.t=?,
    ~tabStyle: ReactNative.Style.t=?,
    ~labelPosition: labelPositionArgs => string=?,
    ~adaptive: bool=?,
    ~style: ReactNative.Style.t=?,
    unit
  ) =>
  bottomTabBarOptions;
```

~(チルダ)はラベル付き引数を表すらしい。
`unit`とは何ぞや？という気持ちになるが、
[ReasonMLを使う理由…ではなく入門](https://qiita.com/oedkty/items/3fe4c68e404689476a58)によれば、
>OCamlにおける経験則として、引数にunit()を配置することをとすることで、
>その位置までの引数を無視する、つまりNoneを指定して関数を実行することを明示できます。

ということらしい。

```reasonml:RootNavigator.res(tabBarOptions)
let tabBarOptions = bottomTabBarOptions(
    ~activeTintColor="tomato",
    ~inactiveTintColor="gray",
    (),
)
```

とし、

```reasonml:RootNavigator.res(tabBarOptionsAdded)
@react.component
let make = () => <>
    <Native.NavigationContainer>
        <Navigator
            tabBarOptions
        >
            <Screen name="main" component=Home.make />
            <Screen name="second" component=Second.make />
            <Screen name="third" component=Third.make />
        </Navigator>
    </Native.NavigationContainer>
</>
```

とすれば、

```jsx
<Tab.Navigator
    tabBarOptions={{
      activeTintColor: 'tomato',
      inactiveTintColor: 'gray',
    }}
>
</Tab.Navigator>
```

と同等になる。なお、JavaScriptではタグ内のプロパティ単独表記はプロパティに`true`が代入されていたが、ReScriptではプロパティと同名の変数の値が入る。JavaScriptのオブジェクト作成時と同じ感覚。
[ReScript Docs / Language Manual / Overview #JSX](https://rescript-lang.org/docs/manual/latest/overview#jsx)

![use tabBarOptions](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/1707a706-d9b4-1d9f-b363-992c731916c6.png)


## screenOptions-Ionic

次に`screenOptions`にとりかかる。

### screenOptionsの型

`Tab.Navigator`のProps

```reasonml:reason-react-navigation/src/BottomTabs.re(navigatorProps)
type navigatorProps = {
  initialRouteName: option(string),
  screenOptions: option(optionsCallback),
  backBehavior: option(string),
  _lazy: option(bool),
  tabBar: option(Js.t(bottomTabBarProps) => React.element),
  tabBarOptions: option(bottomTabBarOptions),
};
```

より、`screenOptions`の型は`option(optionsCallback)`。

`optionsCallback`の型は

```reasonml:reason-react-navigation/src/BottomTabs.re(optionsCallback)
type optionsCallback = optionsProps => options;
```

で、 `optionsProps`, `options`の型は

```reasonml:reason-react-navigation/src/BottomTabs.re(optionsProps)
type optionsProps = {
  navigation,
  route,
};
```

と

```reasonml:reason-react-navigation/src/BottomTabs.re(options)
[@bs.obj]
external options:
  (
    ~title: string=?,
    //TODO: dynamic, missing static option: React.ReactNode
    ~tabBarLabel: tabBarLabelArgs => React.element=?,
    ~tabBarIcon: tabBarIconArgs => React.element=?,
    ~tabBarAccessibilityLabel: string=?,
    ~tabBarTestID: string=?,
    ~tabBarVisible: bool=?,
    ~tabBarButton: React.element=?,
    ~unmountOnBlur: bool=?,
    unit
  ) =>
  options;
```

となる。今回用いる`tabBarIcon`の型`tabBarIconArgs => React.element=?`の引数`tabBarIconArgs`の型は

```reasonml:reason-react-navigation/src/BottomTabs.re(options)
type tabBarIconArgs = {
  focused: bool,
  color: string,
  size: float,
};
```
となっている。

これらに基づいて組み立てていく。大枠は以下。

```reasonml
let screenOptions = ({navigation, route}: optionsProps) => {

    @react.component
    let selecter = ({focused, color, size}: tabBarIconArgs): React.element => {
　　　　　// 何らかの処理
        <>
          // アイコンのコンポーネント
        </>
    }

    options(
        ~tabBarIcon=selecter,
        (),
    )
}
```

### フォント

今回は[react-native-vector-icons](https://github.com/oblador/react-native-vector-icons)のIonicを使う。

まず、yarnでインストールし、

```bash
yarn add react-native-vector-icons bs-react-native-vector-icons
```

`bsconfig.json`に`bs-react-native-vector-icons`を加える。

```json:bsconfig.json(bs-dependencies)
"bs-dependencies": [
  "reason-react",
  "reason-react-native",
  "reason-react-navigation",
  "bs-react-native-vector-icons"
]
```

また、[Installation](https://github.com/oblador/react-native-vector-icons#installation)で言及されている通り、各OS毎にフォントを利用できるようにするための手順があるので、それに従いプロジェクトファイルに変更を加える。

```reasonml:RNIcons.re(Ionicons)
module Ionicons = {
  [@bs.deriving jsConverter]
  type name = [
    | [@bs.as "ios-add"] `_iosAdd
    | [@bs.as "ios-add-circle"] `_iosAddCircle
    // (中略)
    | [@bs.as "md-woman"] `_mdWoman
  ];
  let nameToJs = nameToJs;

  [@bs.module "react-native-vector-icons/Ionicons"] [@react.component]
  external make:
    (~name: string, ~color: string=?, ~size: float=?) => React.element =
    "default";

  let makeProps = (~name) => makeProps(~name=nameToJs(name));
};
```

ReasonML/ReScriptに詳しくなく、ライブラリに使い方が書いてなくて困った。

```reasonml
Ionicons.make({"name":"ios-home", "color":Some(color), "size":Some(size)})
```

で型を合わせたつもりが、`Cannot call a class as a function`エラーで動かず。

```reasonml
<Ionicons name color size />
```

の`name`に文字列を入れるも動かず。

何時間もわからんといいながらたどり着いたのが、類似のライブラリ[reason-expo-vector-icons](https://github.com/lewisf/reason-expo-vector-icons)のページ。

```reasonml
<Ionicons name=`md-checkmark-circle size=32 color="green" />
```

なるほど。`hogeがENUMみたいなやつなのね、と。(多相variantというらしい

この例のように、RNIcons.reに存在する`_iosHomeをnameに入れてみる

```reasonml
<Ionicons name=`_iosHome color size />
```

残念ながら動かない。だってこのコードはReasonMLじゃなくてReScriptだから。
[ReScript Docs / Language Manual /　MigrateFromBucklescriptReason](https://rescript-lang.org/docs/manual/latest/migrate-from-bucklescript-reason)

>Polymorphic variants: from `red to #red.

多相variantの指定方法は`redから#reｄに変更。なるほど。

```reasonml
<Ionicons name=#_iosHome color size />
```

でFA。

あとはroute.nameでパターンマッチを行い、タブによるアイコンの出し分けを行う。

```reasonml:RootNavigator.res(screenOptions)
let screenOptions = ({route}: optionsProps) => {

    @react.component
    let selecter = ({color, size}: tabBarIconArgs): React.element => {
        let name = switch route.name {
            | "main" => #_iosHome
            | "second" => #_iosList
            | "third" => #_iosHeart
            | _ => #_iosInformationCircle
        }
        <Ionicons name color size />
    }

    options(
        ~tabBarIcon=selecter,
        (),
    )
}
```

NavigatorにもscreenOptionsを加えてやる。

```reasonml:RootNavigator.res(tabBarOptionsAdded)
@react.component
let make = () => <>
    <Native.NavigationContainer>
        <Navigator
            screenOptions
            tabBarOptions
        >
            <Screen name="main" component=Home.make />
            <Screen name="second" component=Second.make />
            <Screen name="third" component=Third.make />
        </Navigator>
    </Native.NavigationContainer>
</>
```

![result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/d946ee41-71bc-e3f1-a56f-5b0bf9d26642.png)

無事Tabにアイコンが表示された。
めでたし。めでたし。

# 実装結果

GitHubに上げてみた。iOS Simulatorだけで試してたので、Androidで動かす場合はフォントの設定が必要。
[rescript-react-navigation-tab-sample - GitHub](https://github.com/Nagatatz/rescript-react-navigation-tab-sample)

# 感想

仕事でReactNativeを使ったりしていて、これまで書いていた最新のECMAScript準拠のJavaScriptに代わる言語として、TypeScriptよりはReScriptを使いたい気持ちがあるが、記事やサンプルコードがほとんど転がっていないので、ハマったときの時間のロスが大きいなぁ、という気持ちになった。なかなかサポートがない状態ではこの言語で業務開発は難しそう。チーム開発では自分のトラブルシュートに加え、仲間のサポートもしなくてはいけなくなるし。美しく書けるというのは良いのだが、納期も予算もあるんだよ…。

あと、最初にReScriptの説明書こうと思ったけど何も思いつかなくてやめた。BuckleScript+ReasonML=ReScript？ ReasonMLの時も文法変わってた気がするし、BuckleScriptを書いたことがないからわからない…。

# 参考文献
- [ReScript](https://rescript-lang.org/)  
  - [EditorPlugins](https://rescript-lang.org/docs/manual/latest/editor-plugins)  
  - [Overview #JSX](https://rescript-lang.org/docs/manual/latest/overview#jsx)
  - [MigrateFromBucklescriptReason](https://rescript-lang.org/docs/manual/latest/migrate-from-bucklescript-reason)
- [Reason React Native](https://reason-react-native.github.io/)  
  - [Create a new project with Reason React Native](https://reason-react-native.github.io/en/docs/install/)  
- [reason-react-navigation - GitHub](https://github.com/reason-react-native/reason-react-navigation) 
  - [BottomTabs.re](https://github.com/reason-react-native/reason-react-navigation/blob/master/src/BottomTabs.re)  
- [reason-react-navigation-example - GitHub](https://github.com/reason-react-native/reason-react-navigation-example)  
- [React Navigation](https://reactnavigation.org/)  
  - [Tab Navigation](https://reactnavigation.org/docs/tab-based-navigation)  
- [react-native-vector-icons - GitHub](https://github.com/oblador/react-native-vector-icons)  
- [bs-react-native-vector-icons - GitHub](https://github.com/Astrocoders/bs-react-native-vector-icons)  
  - [RNIcons.re](https://github.com/Astrocoders/bs-react-native-vector-icons/blob/master/src/RNIcons.re)  
- [Reason @expo/vector-icons - GitHub](https://github.com/lewisf/reason-expo-vector-icons)  
- [ReasonMLを使う理由…ではなく入門 @oedkty - Qiita 2020年02月11日更新](https://qiita.com/oedkty/items/3fe4c68e404689476a58)