LoginSignup
8
8

More than 3 years have passed since last update.

ReactNative + Firestoreで簡単な無限スクロールページ

Last updated at Posted at 2019-10-21

Firestoreで多量のデータを作り、ReactNativeのコンポーネントを使って以下のような簡単な無限スクロールを実装してみます。
スクリーンショット 2019-10-21 11.13.27.png

テストデータの作成

前の記事の続きになります。
Cloud Functionsを使って、認証ユーザーが作成された時に多量のデータをユーザーに紐付けて作成してみます。
今回はauthor(ユーザーのuid)、textorderというフィールドをもつpostsコレクションを追加しました。

functions/index.js(部分)
const db = admin.firestore();

exports.createUser = functions.auth.user().onCreate((user) => {
  const SIZE = 100;
  const batch = db.batch();
  const userRef = db.collection('/users').doc(user.uid);
  batch.set(
    userRef,
    {
      email: user.email,
      postsSize: SIZE
    }
  );
  for (let i = 0; i < SIZE; i++) {
    batch.set(
      userRef.collection('/posts').doc(),
      {
        text: 'test',
        order: i
      }
    );
  }
  batch.commit();
});

データベースのセキュリティルールはこのように設定しておきます。

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth.uid == userId;
      match /posts/{post} {
        allow read, write: if request.auth.uid == userId;
      }
    }
  }
}

100個なのでそんなに多量ではないですが、Firestore.batchを使って、usersの中のサブコレクションとして一括書き込みをするようにしました。
Firestoreでは低コストでドキュメント数をカウントする関数が無いようなので、postsの数をusersのフィールドとして保持しておいて、追加の度にインクリメントするようなのを想定しています。
これをデプロイし、ユーザー登録してみます。
expo-auth-practice_–_Database_–_Firebase_console.png
ユーザー情報にpostsコレクションが作られ、テストデータが保存されました。

では、一覧画面を作ります。

List.js
import React, { Component } from 'react';
import { View, FlatList, ActivityIndicator } from 'react-native';
import { ListItem } from 'react-native-elements';
import { connect } from 'react-redux';
import { getPosts } from '../redux';

class List extends Component {
  constructor(props) {
    super(props);
    this.startLoading = this.startLoading.bind(this);
    this.state = {
      isLoading: false
    };
  }

  componentDidMount() {
    const { posts } = this.props;
    if (!posts) {
      this.startLoading();
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.posts !== this.props.posts) {
      this.setState({
        isLoading: false
      })
    }
  }

  startLoading() {
    const { posts, getPosts } = this.props;
    this.setState({
      isLoading: true
    }, () => {
      getPosts(20, (posts && posts.length) ? posts[posts.length - 1] : null);
    });
  }

  render() {
    const { dbUser, posts } = this.props;
    const { isLoading } = this.state;
    const isMore = posts && posts.length < dbUser.postsSize;
    const isLoadingEnabled = !isLoading && isMore;
    const activityIndicator = isMore ? (
      <ActivityIndicator
        animating
        size="large"
        style={{
          marginTop: 16,
          marginBottom: 16
        }}
      />
    ) : null;
    return (
      <View
        style={{
          flex: 1,
          height: '100%'
        }}
      >
        {posts ? (
          <FlatList
            data={posts}
            keyExtractor={(item) => (item.id)}
            renderItem={({ item }) => (
              <ListItem
                title={item.data().order.toString()}
                subtitle={item.data().text}
                bottomDivider
              />
            )}
            onEndReached={isLoadingEnabled ? this.startLoading : null}
            onEndReachedThreshold={0}
            ListFooterComponent={activityIndicator}
          />
        ) : null}
      </View>
    );
  }
}

const mapStateToProps = state => ({
  dbUser: state.user.dbData,
  posts: state.user.posts
});

const mapDispatchToProps = {
  getPosts
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(List);

ReduxのstoreからDBのユーザー情報、DBのテストデータを取って来るとして、コンポーネントが初期化されたらデータの取得を開始するアクションを呼ぶようにします。

一覧画面の描画にはreact-nativeのFlatListとreact-native-elementsのListItemを使ってみます。

FlatListonEndReachedにリストの一番下までスクロールした時のトリガー関数を指定できるので、そのタイミングでもデータ取得を開始するようにします。ローディングの状態変化やそれ以上読み込み可能かどうかの判断など、実装パターンは大体何を使うのでも同じだと思うので、好きな書き方でよさそうです。

onEndReachedが呼ばれるタイミングはonEndReachedThresholdで変えることができ、リストの一番下からの距離を、リストの見える範囲の高さ(全体の高さではなく)を1として指定できます。デフォルトは0.5ですが今回は0を指定し、一番下まで完全にスクロールしてから読み込むようにしてみました。

getPostsアクションはこのようにしました。

redux.js(部分)
export const getPosts = (size, startAfter) => (dispatch) => {
  const user = store.getState().user.data;
  if (user) {
    const userRef = db.collection('/users').doc(user.uid);
    userRef.get()
      .then((userSnapshot) => {
        dispatch({
          type: 'SUCCESS_GET_USER',
          data: userSnapshot.data()
        });
        let postsRef = userRef.collection('/posts')
          .orderBy('order')
          .limit(size);
        if (startAfter) {
          postsRef = postsRef.startAfter(startAfter);
        }
        return postsRef.get();
      })
      .then((querySnapshot) => {
        dispatch({
          type: 'SUCCESS_GET_POSTS',
          posts: querySnapshot.docs
        });
      })
      .catch(({ message }) => {
        Alert.alert(message);
      });
  }
};

読み込む個数と、最後に読み込んだドキュメント(DocumentSnapshot)を指定して取得を開始します。
QueryorderBylimitstartAfterを使って整列、個数指定、位置指定を行っています。
サブコレクションに対するQueryDocumentReferenceにさらにcollectionメソッドを使用することで取得できます。
これでFirestoreでのページング処理ができました。

Github

サンプルソースはこちらです
https://github.com/mildsummer/expo-auth-practice
https://github.com/mildsummer/expo-auth-practice-firebase

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8