ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

FlatListのデータ更新時に再描画されない

概要

ちょっと困ってしまって、検索が時間かかったので他の人の手助けになればと思いメモします。

問題

React NavigationとFlatListでリスト編集サンプルアプリを作成した時に、FlatListのデータ更新時に再描画されない問題に遭遇しました。

  • データをFlatListで表示する画面がある
  • 他の画面でデータを更新する
  • FlatListの画面を開くとデータが描画されていない(更新したものだけでなく、元からあったものの描画されない)
  • ちょっとスクロールすると再描画される

なんだこれ?と悩んでしまいました。

解決

検索するとextraDataを設定しろとか色々見つかったのですが、このIssueから解決できました。

github.com

コメントで言及されているケースと同じ状態だったのでFlatListremoveClippedSubviews={false}を追加してあげると再描画されるようになりました。

感想

removeClippedSubviewsは高速化のためのフラグだそうです。FlatListがラップしているVirtualizedListのドキュメントにこのプロパティに関する言及があります。またこのページに「バグを引き起こすかも」、という注意がされているのも確認できます。

上記の方法でもうまくいかないケースがあるそうなのでIssueを読んでみると何か糸口が見つかるかもしれません。

React Navigationで画面遷移してみる

React Nativeで画面遷移したかったのでまとめました。

内容としてはcreate-react-native-appで作ったアプリで、2つのナビゲーター(StackNavigatorTabNavigator)を使ってみた勉強記事です。

注意としてはDrawerNavigatorは使わないことと、redux等との連携も書いてないことです。

記述方法等は公式ドキュメントをご確認ください。

概要

公式ドキュメント で紹介されている画面遷移用のライブラリはいくつかありますが、 クロスプラットフォームで動作するものと考えると選択肢に入るのは以下の二つかなと思いました。 二つとも綺麗なドキュメントが用意されています。

どちらを選んでもよかったのですが、 create-react-native-appで使ったアプリを書きたいと思ったので、 javascriptだけで書かれているReact Navigationで書くことにしました。

React Navigation

React NavigationはReact Nativeで画面遷移を実装するために、いくつかのナビゲーター(navigator)というコンポーネントを利用します。

ナビゲーターはルーティングしたり画面に引数をあげたりジェスチャーを制御したり、画面遷移に必要なものを提供してくれるものです(ざっくりです)。

標準で使えるナビゲーターとしては、StackNavigatorTabNavigatorDrawerNavigatorです。それぞれ以下のような特徴があります。

  • StackNavigator: 画面を積んでいくタイプのナビゲーター。Screen1 -> Screen2とかScreen1 -> Screen3とか画面を積んでツリーを作っていき、行きつ戻りつしていくという感じです。描画した画面内のコンポーネントを押したら別の画面に遷移させる、といったWebのリンクと同じようなケースで使う感じです。
  • TabNavigator: タブタイプのナビゲーター。[Screen1、Screen2、Screen3]のように並列に画面を依存させずに扱い、切り替えていく感じです。画面遷移はTabNavigator自身が責任を持つ感じです。
  • DrawerNavigator: ドロワータイプのナビゲーター。自分的にはタブが横なら、ドロワーは縦という程度の位置が違うくらいの認識です。ただ、タブに比べて登録できる画面数が多い(多くても比較的わかりにくくならない)ので、例えばユーザーによって画面数が変化するタイプのものであれば、これを使うのかなと思います。

通常はこれらをネストさせるなどしてアプリケーションの画面遷移を作っていきます。カスタムしたナビゲーターを作ることもできるようですが、まだいいかなと思います。(丸いナビゲーターとかARっぽくてカッコ良さそう)

初心者的にはネストさせ方がちょっと不明で困ります。よく見るアプリケーションでは、

  • TabNavigator(下タブ) or DrawerNavigatorをルートの親ナビゲーターとして選ぶ
  • それぞれの子にScreen(普通の画面)、StackNavigator、TabNavigator(上タブ)を任意に登録
  • そのそれぞれの子に画面を登録

のネストが2段くらいの構成になっていると思いますのでそれを真似します。

疑問: 認証、設定などの画面はどのように制御するのがいいのでしょうか?設定画面はモーダルを使って表示するのはよく見る気がします?認証画面もモーダルを使った表示を見ることがありますが、一瞬アプリが表示されてからシュルッと画面を乗っけるとかだと、裏側で画面が動かないように制御しなければいけない気がします。トップレベルのに認証用のナビゲーターをかませてネストを3段にするとか?

準備

ドキュメントを参考にインストールします。

$ create-react-native-app demo-application
$ cd demo-application
$ npm install --save react-navigation

StackNavigator

単純な使い方として、リストを表示して、押したらその詳細画面に遷移する例を書きます。

自動生成されるApp.jsに以下のコードを記述します。

gist.github.com


StackNavigator

という感じになりました。今回は単純な例なので遷移先のDetailは使い回しです。

TabNavigator

2つの画面を持つTabNavigatorを作成します。

  • App
    • MathematicsList: 先ほど作った数学リスト
    • AddMathItemScreen: 数学項目追加画面

やることは3つです、

  1. TabNavigatorのコンポーネントを作成
  2. 項目追加機能の実装
  3. 項目削除機能の実装

Step1 - TabNavigatorのコンポーネントを作成

App.jsを修正してTabNavigatorを描画するコンテナーコンポーネントを作成していきます。

...
import { TabNavigator } from 'react-navigation'; /* 追加 */

/*
 * TabNavigatorを作成
 * StackNavigatorと基本は同じ
 * 第二引数で画面下タブに表示されるアイコン色とラベル非表示を設定
 */
const Tab = TabNavigator({
  List: { screen: MathematicsList },
  AddItem: { screen: AddMathItemScreen },
}, {
  tabBarOptions: {
    activeTintColor: '#037aff',
    inactiveTintColor: '#737373',
    showLabel: false,
  },
});
/*
 * 二つのタブでデータを共有するためにAppコンポーネントをコンテナコンポーネント化
 * 今の所stateを持っている以外はTabNavigatorをラップしているだけ
 */
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      mathematics,
    };
  }

  render() {
    /* screenPropsで各子供にmathematicsを渡している */
    return <Tab screenProps={{ mathematics: this.state.mathematics }} />;
  }
}

TabNavigatorStackNavigatorコンポーネントなのでjsx記法でrenderに書くことができます。screenPropsに値を入れると各タブでデータを受け取ることができるようです。ここでは各タブにmathematicsデータを渡すために、TabNavigatorの設定をして一旦Tabと名付けた後に、Appコンポーネントをコンテナ化し内部でscreenPropsを渡しました。

TabNavigatorで登録された画面にnavigationOptionsを設定していきます。AddMathItemScreenは以下のように作成しました。

...
import { Entypo } from '@expo/vector-icons';
...
/*
 * 項目追加画面を作成
 * 現在はタブアイコンの設定のみのモック
 */
const AddMathItemScreen = () => (
  <View style={styles.container}>
    <Text style={styles.paragraph}>This is AddMathItemScreen</Text>
  </View>
);
AddMathItemScreen.navigationOptions = {
  tabBarIcon: ({ tintColor }) => <Entypo size={24} name="add-to-list" color={tintColor} />,
};

tabBarIcontintColorTabNavigatorのオプションで設定していて、activeinactiveに応じて異なる値が送られてきます。Entypocreate-react-native-appで作成すると初めから使えるexpoライブラリに含まれているカスタムフォントのコンポーネントです。

AddMathItemScreenと同様にMathematicsListにもタブアイコンの設定を行います。

/*
 * StackNavigatorを変数に格納
 */
const Stack = StackNavigator({
  Detail: { screen: DetailScreen },
  List: { screen: ListScreen },
}, {
  initialRouteName: 'List',
});
/*
 * StackNavigatorをラップするコンポーネント
 * screenPropsにより親からもらったpropsを子にそのまま流している(手抜き)
 * mathematicsリストをAppコンポーネントに持たせているため、
 * ここで設定したものがListScreenに渡っていく。
 */
const MathematicsList = ({ screenProps }) => (
  <Stack screenProps={screenProps} />
);
/*
 * タブアイコンの設定
 */
MathematicsList.navigationOptions = {
  tabBarIcon: ({ tintColor }) => <Entypo size={24} name="list" color={tintColor} />,
};

また、親からscreenPropsが渡されてくるのでそれをListScreenに渡すためにここでも一旦StackNavigatorを変数に格納して、MathematicsList内で受け渡しを行います。(もっといい方法があると思うんですが。。。ちゃんとやるときは多分reduxとか使うのでこの辺りはなあなあにしておきます)

ちなみにListScreenは次のように値を受け取ります。

/* 引数screenPropsが親から渡ってくる */
const ListScreen = ({ navigation, screenProps }) => (
  <FlatList
    data={screenProps.mathematics} /* mathematicsを取り出す */
...


TabNavigator1

上記の変更をまとめたのが以下になります。

gist.github.com

2つのタブが画面下に表示され、片方の画面は先ほどのStackNavigatorがネストされている状態になっています。

追記: タブアイコンの設定箇所をTabNavigatorの定義箇所に変更することも可能なようです。StackNavigatorに関しても同様で、タイトルなど動的に変更する必要があるかどうかで分けるという感じです。

const Tab = TabNavigator({
  List: {
    screen: MathematicsList,
    navigationOptions: {
      tabBarIcon: ({ tintColor }) => <Entypo size={24} name="list" color={tintColor} />,
    },
  },
  AddItem: {
    screen: AddMathItemScreen,
    navigationOptions: {
      tabBarIcon: ({ tintColor }) => <Entypo size={24} name="add-to-list" color={tintColor} />,
    },
  },
}, {
  initialRouteName: 'List',
  tabBarOptions: {
    activeTintColor: '#037aff',
    inactiveTintColor: '#737373',
    showLabel: false,
  },
});

Step2 - 項目追加機能の実装

項目追加機能を実装するために、Appコンテナコンポーネントからstateをいじれる関数を、TabNavigator経由でAddMathItemScreenコンポーネントまで渡します。

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      mathematics,
    };
    this.addNewMathItem = this.addNewMathItem.bind(this);
  }

  /* 子供に渡す関数を作成 */
  addNewMathItem({ title, detail }) {
    this.setState({
      mathematics: [...this.state.mathematics, { title, detail }],
    });
  }

  render() {
    /* screenPropsで各子供にmathematics、addNewMathItemを渡す */
    return <Tab screenProps={{ mathematics: this.state.mathematics, addNewMathItem: this.addNewMathItem }} />;
  }
}

受け取ったAddMathItemScreen側でタイトル、詳細のテキスト入力、追加リクエスト処理を記述します。追加が完了したらListScreenに遷移するようにAlertOKボタンをトリガーに設定します。見栄えに関して多少のスタイルも追加しています。

class AddMathItemScreen extends React.Component {
  constructor(props) {
    super(props);
    this.state = { title: '', detail: '' };
    this.handleOnPress = this.handleOnPress.bind(this);
  }

  handleOnPress() {
    const { title, detail } = this.state;
    if (!title) return Alert.alert('Error', 'titleは必須です');

    this.props.screenProps.addNewMathItem({ title, detail });
    Alert.alert(
      'Notice',
      '項目を追加しました!',
      [{ text: 'OK', onPress: () => this.props.navigation.navigate('List') }],
    );
    this.setState({ title: '', detail: '' });
  }

  render() {
    return (
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View style={styles.container}>
          <View style={styles.inputGroup}>
            <Text style={[styles.paragraph, styles.label]}>title</Text>
            <TextInput
              blurOnSubmit
              onChangeText={title => this.setState({ title })}
              style={[styles.textInput, styles.heading]}
              value={this.state.title}
            />
          </View>
          <View style={styles.inputGroup}>
            <Text style={[styles.paragraph, styles.label]}>detail</Text>
            <TextInput
              blurOnSubmit
              multiline
              onChangeText={detail => this.setState({ detail })}
              style={[styles.multiTextInput, styles.paragraph]}
              value={this.state.detail}
            />
          </View>
          <Button onPress={this.handleOnPress} title={'Add item to list'} />
        </View>
      </TouchableWithoutFeedback>
    );
  }
}


TabNavigator2

まとめると次のようになっています。

gist.github.com

Step3 - 項目削除機能の実装

項目追加機能同様、Appコンテナコンポーネントからstateをいじれる関数を渡します。削除の方法としては、配列のインデックスをキーに各詳細画面の削除ボタンから実行します(画面遷移で操作を限定させないと容易にバグりそうですね、理詰めでバグらせられそう)

削除メソッドを追加し、screenPropsで渡します。

export default class App extends React.Component {
...
  removeMathItem(index) {
    this.setState({
      mathematics: this.state.mathematics.filter((_, i) => i !== index),
    });
  }
...
  render() {
    /* screenPropsで各子供に渡している */
    return (
      <Tab
        screenProps={{
          mathematics: this.state.mathematics,
          addNewMathItem: this.addNewMathItem,
          removeMathItem: this.removeMathItem,
        }}
      />
    );
  }

次にDetailScreenのヘッダー右上に削除ボタンを設置する設定を書きます。indexListScreenからの遷移時にnavigation.state.params経由で渡すようにし、removeMathItemscreenProps経由で渡したのでそれらから取得できます。(ListScreenの変更は最後のまとまったもので確認できます)

DetailScreen.navigationOptions = ({ navigation, screenProps }) => ({
  headerRight: (
    <TouchableOpacity
      style={{ marginRight: 8 }}
      onPress={() => {
        Alert.alert(
          'Warning',
          '項目を削除しますか?',
          [
            {
              text: 'Delete',
              onPress: () => {
                screenProps.removeMathItem(navigation.state.params.index);
                navigation.goBack();
              },
            },
            { text: 'Cancel' },
          ],
        );
      }}
    >
      <Entypo size={24} name="trash" color={'red'} />
    </TouchableOpacity>
  ),
});

以上で削除機能が実装できました。コード汚いよ、コンポーネントに切り出せよ、と思いますが指針が決まらないのでとりあえず画面単位の分割に抑えています。

完成

完成しました。こんな感じで動いています。


TabNavigator3

gist.github.com

感想

引数の受け渡しはReduxとかでやりたいと思いました。なのでReduxない状態でのプロパティの渡し方は勉強不足です。すみません。

あと書き捨てのつもりだったのでgit管理していなくて、diffだけ見せるとかいう方法もあったかもという気がします。さらによくよく考えればgistの差分でもよかったです。記事が読みにくい感じですね。

React NativeのNavigatorがなくなった

ちょっと困った

SectionListが使いたいのでreact-nativeのバージョンをあげようと思ったら、 バージョン0.44にはNavigatorコンポーネントがないみたいであげられなくなっちゃいました。

なんか書いてある

Navigatorはお手軽だったので、結構使ってしまっていて他のライブラリで書き直すのはちょっと面倒です。

対策

応急処置としては指示通りにreact-native-deprecated-custom-componentsから使い、徐々に他のナビゲーションで書き換えていくつもりです。

感想

そういえば当時のドキュメントにNavigationExperimental的なコンポーネントが現れたりしてて、ナビゲーションに関して試行錯誤中な匂いはあった気がします。

これを機にナビゲーションを見直すのと、githubをwatchしていこうと思います。

追記

バージョンアップして試しにimport { Navigator } from 'react-native'してみたら、赤い画面で上の内容をディレクションしてくれますね。

動かなくなったりして迷うことはあまりないかもですね。

Reactコンポーネントをディレクトリにまとめる(ES6)

Reactのディレクトリ構成が全然わかりません。

Reactコンポーネントを分割する方法についてもメモです。ES6の文法の話だと思うのですが、正式なドキュメントが見つけられていないので間違いがあったら申し訳ないです。

書いたものをgithubに上げています。参考になれば幸いです。

webpackを使ってビルドを行なっており、設定ファイルは以下のようになっています;

  ...
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
          presets: ['es2015', 'stage-2', 'react']
        }
      }
    ]
  }

ディレクトリによる分割

例として以下のようにディレクトリを構成してみました。

src/
├── App
│   ├── App.js
│   ├── Bar.js
│   ├── Baz.js
│   ├── Foo.js
│   └── index.js
└── index.js

index.js内でimport App from './App'のようにモジュールとしてディレクトリを指定すると、ファイル./App/index.jsの内容がコンポーネントとして読み込まれます。./App/index.jsディレクトリ内のコンポーネントを組み立てておけば、呼び出し側はあたかも一つのコンポーネントのように扱うことができます。

ディレクトリでまとめることで、意味を保ったままコンポーネントの分割が行えそうです。「components」ディレクトリ以下にコンポーネントが大量に平置きされてしまうのを防げました。

感想

一方でコンポーネントの再利用性が下がる気もします。
一部のコンポーネントは他の箇所から再利用、他はディレクトリ内のコンポーネントを利用などしてしまうと、参照がわけわからなくなってしまいます。相変わらずディレクトリ構成は悩みどころです。
ちゃんと部品単位でのコンポーネント化を意識すると多少は回避できるかもしれません。

Railsからキーがキャメルケースのjsonを返す(jbuilder)

 Reactでアプリを書いていて思ったことのメモです。

 大概jsのオブジェクトのプロパティはキャメルケース(camel case)ですよね。
 Railsをサーバサイドにしたのですが、返却されるjsonがスネークケース(snake case)でした。

 クライアントサイドで書き換えるのも面倒だと思ったので、調べてみたところRails側で対応するのが楽なようです。

内容

 Rails(4から?)はjsonをリクエストした際に返却するオブジェクトの定義を、jbuilderというテンプレートエンジンで行うようです。

生成されている(例: _model.json.jbuilder)ファイルの先頭に

json.key_format! camelize: :lower
...

と記述するとキャメルケースに変更できました。

感想

 ちゃんとしたapiを作るならkey_formatも引数で指定できたほうがいいのだろうか?と思いましたが使う気がしないので何もしませんでした。
 クライアントからサーバへの送信の際にも書き換えが必要になると思いますが、どうしようかなーと考え中です。

 json.key_format!は単に文字列変換の関数を引数にとり、keyに適応する関数のようです。ここではcamelize: :lowerを渡していますが、他にも色々渡せますね。
 この関数はそんなに使い込む機会はないと思いますが、私は関数を引数に取る関数が結構好きです。だからどうしたという話ですが。

rubydoc: Jbuilder:key_format!