Laravel + ReactNative(FlatList)で無限スクロール


## 無限スクロールとは？

正式な呼び方はわかりませんが、リストが最後まで来たら、次のページを読み込む処理。SNSのタイムライン読み込みとかで普通に見るやつ。
サーバ側との連携が不可欠なので、いつも使うLaravel + ReactNativeで試す。

## Laravel側の実装

まず、Laravel側の準備。とりあえず標準で準備されているUser関連のデータを利用する。

### マイグレーション

.envでデータベースの設定をしたら、migrateとするだけでusersテーブルが生成されます。

```bash
php artisan migrate
```

### データ生成

最初からあるFactoryを使ってデータを生成します。
DatabaseSeeder.phpに直接下記のように記述します。数はお好みで。

```php:database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        //Userデータを100件作る
        factory(App\User::class,100)->create();
    }
}

```

seederを実行します。

```bash
php artisan db:seed
```

これでデータができました。

### ルート定義

続いて値をJSONで返すAPIを定義していきます。まずはルートから。
/api/usersでアクセスしたら値を返す想定。

```php:web.php
<?php

use Illuminate\Http\Request;

Route::get('/users', 'UserController@index');

```

### コントローラー定義

続いてControllerの作成。

```bash
php artisan make:controller UserController
```

10件ずつページネーションするのは下記のような記述でOK。

```php:UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index(){
        $users = \App\User::paginate(10);
        return \Response::json($users);
    }
}
```

### リクエスト

問題はLaravelがどうページネーションをしてくれてるか？

とりあえず起動。

```bash
php artisan serve
```

#### 普通にリクエストしてみる。

http://localhost:8000/api/users

どうやら1ページ目が帰ってきてるよう。
重要なことは、Userデータ以外にもnext_pageやlast_pageなどが帰ってきてること。

特に何ページあるかは重要なのでlast_pageは重要な値。

で、どうやってページを送るのか？

```json
{"current_page":1,"data":[{"id":1,"name":"Toney Marvin III","email":"dolly17@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":2,"name":"Kayli Walsh","email":"xgibson@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":3,"name":"Jamal Schmitt","email":"fjones@example.net","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":4,"name":"Kurtis Feeney","email":"arnaldo.mante@example.org","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":5,"name":"Alexandro Walsh","email":"skshlerin@example.org","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":6,"name":"Jessyca Towne","email":"wilmer.labadie@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":7,"name":"Favian Parker MD","email":"ccummerata@example.org","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":8,"name":"Osbaldo Jenkins","email":"ettie.parker@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":9,"name":"Dr. Beryl Nienow V","email":"justice72@example.net","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":10,"name":"Mr. London Spinka","email":"mazie.luettgen@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"}],"first_page_url":"http:\/\/localhost:8000\/api\/users?page=1","from":1,"last_page":10,"last_page_url":"http:\/\/localhost:8000\/api\/users?page=10","next_page_url":"http:\/\/localhost:8000\/api\/users?page=2","path":"http:\/\/localhost:8000\/api\/users","per_page":10,"prev_page_url":null,"to":10,"total":100}
```

#### ページ送り(page=2)

page=ページ番号というパラメータを付けて送ってみると、そのページが返る。素晴らしい。
ということはpageをlast_pageまでカウントしていけば、ページ送り（というか、ページの追加）ができるということです。

http://localhost:8000/api/users?page=2

```json
{"current_page":2,"data":[{"id":11,"name":"Dean Krajcik","email":"minnie.harvey@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":12,"name":"Ms. Rachelle Dibbert PhD","email":"marilou19@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":13,"name":"Gerson Emmerich Sr.","email":"mikel.schuster@example.net","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":14,"name":"Abner Kunde","email":"hmcclure@example.net","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":15,"name":"Prof. Sophia Rogahn V","email":"jaskolski.robin@example.org","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":16,"name":"Prof. Marshall Rodriguez II","email":"srowe@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":17,"name":"Prof. Kian O'Keefe","email":"beatrice.reichert@example.org","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":18,"name":"Prof. Mary Swaniawski MD","email":"mrenner@example.org","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":19,"name":"Mrs. Darby Homenick","email":"whowe@example.com","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"},{"id":20,"name":"Javon Price III","email":"mohammed96@example.net","email_verified_at":null,"created_at":"2018-10-29 21:47:36","updated_at":"2018-10-29 21:47:36"}],"first_page_url":"http:\/\/localhost:8000\/api\/users?page=1","from":11,"last_page":10,"last_page_url":"http:\/\/localhost:8000\/api\/users?page=10","next_page_url":"http:\/\/localhost:8000\/api\/users?page=3","path":"http:\/\/localhost:8000\/api\/users","per_page":10,"prev_page_url":"http:\/\/localhost:8000\/api\/users?page=1","to":20,"total":100}
```

Laravel側は以上です。

## React Native側の実装

で、ReactNative側。とりあえず標準で利用できるもので対応してみる。
以下の通り。

* FlatListのonEndReached()で最後まで行ったかを確認。
* onEndReachedThresholdはどこまで読み込まれたらendとするか（とりあえず0として最後までとする）
* _fetchで読み込むが、newPageの+1ページを読み込み、表示配列のお尻に追加
* 後は自動バインディングでデータが追加・表示される
* last_page以下なら上記の処理を繰り返し、last_page以上では何もしない
* 1000ページあっても読み込まれるので、ページ数に関係なく条件値(max_page)も設定

という感じ。


```js:App.js
import React from 'react';
import { StyleSheet, Text, View, FlatList } from 'react-native';

export default class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      data: [],     //描画するデータ（追記されていく）
      page: 0,      //現在のページ
      last_page: 1, //last_pageで上書きされる
      max_page: 100 //ページ数に関係なく上限を設置（last_pageが150でも100で止まる）
    }
  }

  componentDidMount() {
    this._fetch();
  }

  _fetch = () => {

    if (this.state.page < this.state.last_page && this.state.page < this.state.max_page) {

      const newPage = this.state.page + 1;

      fetch(`http://localhost:8000/api/users?page=${newPage}`)
        .then(
          (response) => response.json()
        )
        .then((responseJson) => {
          this.setState({
            data: [...this.state.data, ...responseJson['data']], //今の配列に取得分を追加
            page: newPage,
            last_page: responseJson['last_page']
          })
          // console.log(responseJson['last_page'])
        })
        .catch((error) => {
          console.log(error);
        })

    }else{
      //何もしない
    }
  }

  render() {
    return (
      <View style={{ flex: 1, paddingVertical: 40 }}>
        <FlatList
          data={this.state.data}
          extraData={this.state.data}
          keyExtractor={(item) => item.id.toString()}
          renderItem={({ item }) => (
            <View style={{ padding: 10 }}>
              <Text
                style={{ fontSize: 24 }}
                onPress={() => alert(item.id)}
              >{item.id} {item.name} {item.email}</Text>
            </View>
          )}
          onEndReached={() => this._fetch()}
          onEndReachedThreshold={0}
        />
      </View>
    );
  }
}

```

>ListFooterComponent={()=>\<ActivityIndicator size='large' animating />}を追加することで、ロード中、footerにspinnerが表示されます。

比較的簡単。便利。[画面を引っ張って更新](https://qiita.com/zaburo/items/2b7d3ee3c32991971621)も簡単。

## 追記（2018年11月25日）

axiosつかったり、spinner入れたり、ListItem利用したりした改良版。

必要なライブラリのインストール。

```bash
npm install --save react-native-elements axios
```

以下実装。

```js:App.js
import React from 'react';
import { StyleSheet, Text, View, FlatList, ActivityIndicator } from 'react-native';
import { ListItem } from 'react-native-elements';
import axios from 'axios';

export default class App extends React.Component {

  state = {
    data: [],
    page: 0,
    last_page: 1,
    max_page: 100,
    animating_status: true,
  }

  componentDidMount() {
    this.getDataFromLaravel();
  }

  render() {
    return (
      <View style={{ flex: 1, paddingVertical: 40 }}>
        <FlatList
          data={this.state.data}
          // extraData={this.state.data}
          keyExtractor={(item) => item.id.toString()}
          onEndReached={() => this.getDataFromLaravel()}
          onEndReachedThreshold={0}
          ListFooterComponent={() => <ActivityIndicator size='large' animating={this.state.animating_status} />}
          renderItem={({ item }) => (
            <ListItem
              key={item.id}
              title={item.id + ' ' + item.name}
              subtitle={item.email}
              hideChevron
              onPress={() => alert(item.id)}
              roundAvatar
              avatar={{uri: 'https://s3.amazonaws.com/uifaces/faces/twitter/ladylexy/128.jpg'}}
            />
          )}
        />
      </View>
    );
  }

  getDataFromLaravel = () => {

    if (this.state.page < this.state.last_page && this.state.page < this.state.max_page) {

      const newPage = this.state.page + 1;

      axios
        .get(`http://localhost:8000/api/users?page=${newPage}`)
        .then(res => {
          this.setState({
            data: [...this.state.data, ...res.data.data],
            page: newPage,
            last_page: res.data['last_page'],
          });
        })
        .catch(error => {
          console.log(error);
        })

    } else {
      this.setState({
        animating_status: false,
      });
      alert('これ以上読み込めません。');
    }
  }
}
```
>なぜかAvatarの書き方は古い？表記じゃないと表示されませんでした（本家、サンプル通りじゃ動かない）。