react-native-router-fluxをいい感じに使う3つの方法

はじめに

最近React Nativeをいじっていて、react-native-router-flux周りの知見が溜まってきたので忘れないうちに書き留めておこうと思う。

react-native-router-fluxとは

簡単に言うと、ネイティブアプリの画面遷移をreact-routerっぽく書けるようにするライブラリ。
React Nativeには画面遷移を扱う仕組みとしてデフォルトでNavigatorが用意されているんだけど、SPAを作ったことのある人にはreact-native-router-fluxの方がとっつきやすいのかなと思う。

react-native-router-fluxでは、次のようにルーティングを書くことができる。

// App.js
import { Router, Scene } from 'react-native-router-flux';

export default function App() {
  return (
    <Router>
      <Scene key="root">
        <Scene key="login" component={Login} title="Login" />
        <Scene key="register" component={Register} title="Register" />
        <Scene key="home" component={Home} />
      </Scene>
    </Router>
  );
}

// index.ios.js
AppRegistry.registerComponent('ExampleApp', () => App);

そして、Actions.key名に設定された関数呼び出すことで、上で定義した各画面(Scene)に遷移することができる。
例えば、タップするとHome画面に遷移するコンポーネントは次のように書くことができる。

import { Actions } from 'react-native-router-flux';

<Text onPress={Actions.home}>Home</Text>

全体的に、なんかこれどこかで見たことある、って感じですね。
このとっつきやすさもあって、現在GitHubでのスターが2800超の人気ライブラリになっている。

いい感じに使う方法

というわけで、以下が本題。

1. Reduxと組み合わせる際の工夫

Reduxとの統合については、基本的には公式のドキュメント通りにやればOK。
ただ、ReducerがStage 3のObject Spread Propertiesを使って書かれていたので、immutability-helperを使う形に直した。

// src/routers/routes.js
import update from 'immutability-helper';
import { ActionConst } from 'react-native-router-flux';

export default function routes(state = initialState, action) {
  switch (action.type) {
    case ActionConst.FOCUS:
      return update(state, { $merge: { scene: action.scene } });
    default:
      return state;
  }
}

また、Providerとルーティングの定義を別ファイルに分離することで、メンテナンスしやすい形にした。

// src/providers/App/index.js
import Router from './Router';

export default function App() {
  return (
    <Provider store={store}>
      <Router />
    </Provider>
  );
}

2. ルーティングとスタイル定義の分離

react-native-router-fluxでは、RouterコンポーネントのPropsにnavigationBarStyleなどを渡すことで、NavBar/TabBarのデザインを修正できる。
しかし、この方法だと同じファイル内でルーティングとスタイル定義を行うことになるので、個人的に非常に微妙に感じていた。
そこで、Routerの中ではNavBar/TabBarをラップしたコンポーネントを指定するだけにして、ルーティングとスタイル定義を分離した。

// src/providers/App/Router.js
import { connect } from 'react-redux';
import { Router as DefaultRouter, Scene } from 'react-native-router-flux';

const DefaultRouterWithRedux = connect()(DefaultRouter);

export default function Router() {
  return (
    <DefaultRouterWithRedux navBar={NavBar}>
      <Scene key="root">
        <Scene key="tabBar" component={TabBar} tabs>
          // ここでルーティングを定義する
        </Scene>
      </Scene>
    </DefaultRouterWithRedux>
  );
}

NavBarはこんな感じの関数になっていて、中でスタイル定義のためのPropsを差し込んでいる。

// src/components/NavBar/index.js
import { NavBar as DefaultNavBar } from 'react-native-router-flux';

export default function NavBar(props) {
  const newProps = update(props, {
    navigationState: {
      barButtonIconStyle: { $set: styles.barButtonIcon },
      navigationBarStyle: { $set: styles.navigationBar },
      titleStyle: { $set: styles.title }
    }
  });

  return <DefaultNavBar {...newProps} />;
}

あとは、src/components/NavBar/styles.jsとかにスタイル定義を書いていけばOK。

3. Storybookを使ったデザイン改善

ルーティングとスタイル定義を分離しておけば、react-native-storybookを使ってNavBar/TabBarのデザインだけ修正、なんてこともできるようになる。
ただ、表示するためのPropsを作るのがちょっと面倒。

// storybook/stories/NavBar.js
import React from 'react';
import { storiesOf, action } from '@kadira/react-native-storybook';
import update from 'immutability-helper';

import NavBar from '../../src/components/NavBar';

const baseChild = {
  key: 'key',
  title: 'Sample Title',
  component: {}
};

const baseProps = {
  component: {},
  navigationState: {
    index: 0,
    children: [baseChild]
  },
  position: {
    interpolate() {}
  }
};

storiesOf('NavBar')
  .add('with title', () =>
    <NavBar {...baseProps} />
  )
  .add('with title and back button', () => {
    const child = update(baseChild, {
      onBack: { $set: action('pressed') }
    });

    const props = update(baseProps, {
      navigationState: {
        parentIndex: { $set: 1 }, // Set any values except 0
        children: { $set: [child] }
      }
    });

    return <NavBar {...props} />;
  });

iOSシミュレータでの表示はこんな感じ。

f:id:Yasaichi:20161203170715p:plain

TabBarの方も修正できるようにしたかったんだけど、Propsが作れなくて断念した。
もっといいやり方を考えるべきかもしれない。

おわりに

使っている途中でバグを踏んだので直してプルリクエスト出したんだけど、かれこれもう3週間くらい放置されてる><