【Next.js】Firestoreから任意のデータを取得して描画する

f:id:nekorokkekun:20190925114408p:plain:w1000
今回はNext.jsでFirebaseから任意のデータを取得して描画する方法について解説をしていきます。

ウェブアプリを作成する際、レイアウトは決めておいてコンテンツの名前や説明文などはデータを取得してレイアウトにはめ込みたい、という場合がありますね。

このように動的なページを作成するための方法を以下で紹介します。

前提

Firebase Cloud Firestore保存したデータを取得することになります。

すでにFirebaseでプロジェクトが作成されており、Firestoreにデータがいくつか登録されているという前提で進みます。

まだFirebaseのプロジェクト作成やデータの登録ができていないという場合は以下の記事を読み、

  • Firebaseでプロジェクトの作成
  • データの登録
  • Next.jsプロジェクトの作成
  • FirebaseとNext.jsプロジェクトのDB接続

を済ませておいてください。

nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com

また上記ではわざわざデータ登録するためにUIを作成していますが、プロジェクトを作成後、サイドメニューからDatabaseを選ぶことでブラウザからコレクション・ドキュメントの登録(=データの登録)を行うことができます!

Firestoreの確認

まずはFirestoreにデータが保存できるかどうかの確認をしましょう。
ブラウザ上でFirebaseのコンソールからプロジェクト>サイドメニューのDatabaseで確認できます。

実際に見ると以下のようにデータが登録されていることがわかります。
f:id:nekorokkekun:20190925104133p:plain

FirestoreはNoSQLなのでMySQLなどのようにテーブル、カラム、レコードといったものはないのですが、イメージとしては

  • コレクション = テーブル
  • ドキュメント = レコード
  • データ = カラムと実際のデータ

と考えるとしっくり方も多いかもしれません。

Firestoreでは以下のようなイラストで図解を出してくれています。
f:id:nekorokkekun:20190925104420p:plain

データの一覧表示

まずは先程確認したデータを一覧表示できるページを作成しましょう。

pages/index.js

import { db } from '../lib/db';
import React from 'react';
import Link from 'next/link';

export default class Index extends React.Component {
  static async getInitialProps() {
    let result = await
      db.collection('fanPages')
      .get()
      .then(snapshot => {
        let data = []
        snapshot.forEach((doc) => {
          data.push(
            Object.assign({
              id: doc.id
            }, doc.data())
          )
        })
        return data
      }).catch(error => {
        return []
      })
    return {datas: result}
  }

  render() {
    const firestoreDatas = this.props.datas
    return (
        <div>
            <h3>Firestoreのデータ一覧</h3>
            <div>
              <ul>
              {firestoreDatas.map(fanPage =>
                <li key={fanPage.id}>
                    <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}>
                      <a>{fanPage.artistName}</a>
                    </Link>
                </li>
              )}
              </ul>
            </div>
        </div>
        );
        }
    }

これで以下のような画面が表示されたのではないでしょうか。
f:id:nekorokkekun:20190925114157p:plain

長いのでいくつかに分けて解説をします。

getInitialPropsでFirestoreからデータの取得

  static async getInitialProps() {
    let result = await
      db.collection('fanPages')
      .get()
      .then(snapshot => {
        let data = []
        snapshot.forEach((doc) => {
          data.push(
            Object.assign({
              id: doc.id
            }, doc.data())
          )
        })
        return data
      }).catch(error => {
        return []
      })
    return {datas: result}
  }

getInitialPropsはクラスコンポーネントで使用できるメソッドです。
コンポーネント生成時にpropsを自動取得してくれるという便利なメソッドなので覚えておきたいですね。

肝心の中身も見ていきましょう。

Firestoreからデータ取得するコレクションを指定
  static async getInitialProps() {
    let result = await
      db.collection('fanPages')
      .get()
      .then(

まず「db.collection('fanPages')」でコレクション「fanPages」からデータ取得を行うことを宣言します。

「db」は変数で、Firestoreの接続設定を他ファイルから取得済みです。詳しくはこちらをご覧ください。

また上記のコードでは非同期処理も行なっています。

「async」と「await」が書かれている部分ですね。
重要なのは「await」を宣言する場所で、意味としては

「result以降の処理が終わるまで結果をreturnしないでね」

ということになります。

Firestoreからのデータ取得は時間が掛かりますので、awaitを配置しておかなければ、データ取得前に「result」がreturnされてしまうため、結果「空っぽのresult」しか返ってこないということになります。

Firestoreのデータを取得して配列に入れる
snapshot => {
        let data = []
        snapshot.forEach((doc) => {
          data.push(
            Object.assign({
              id: doc.id
            }, doc.data())
          )
        })
        return data
snapshotとは?
snapshot => {

こんな1行がありますが、「snapshot」とはFirestoreのその「瞬間」のデータが格納されているものです。
データ取得の際に写真を取るイメージで、その瞬間のデータをsnapshotと名付けているんですね。

データをドキュメントごとに配列に収める
          data.push(
            Object.assign({
              id: doc.id
            }, doc.data())

snapshotの1行上で「let data = []」と、空の配列を用意しました。
その後、Object.assignでdataの中にオブジェクト形式でFirestoreのデータをドキュメントごとに格納しています。

実際、この状態のdataをconsole.logで見てみると以下のように出力されています。

[ { id: 'JNTwv1d5W0g1yXQr667j',
    artistName: 'test1',
    body: 'test1',
    category: 'singer',
    monthlyFee: '1000',
    pageName: 'test1' },
  { id: 'aW749fTXcm99meV7x2WB',
    artistName: 'test2',
    body: 'test2',
    category: 'calligrapher',
    monthlyFee: '2000',
    pageName: 'test2' } ]

一番外側に来ているがdata = で定義した配列ですね。
そして、その中にドキュメントごとにオブジェクト型に整形されたFirestoreのデータが格納されていることがわかります。

ここで配列にわざわざ格納するのは、後ほどmap関数で展開するためです。

そして最後に「return data」で中身の入ったdataが「let result」で定義した「result」の中に入りました。

エラーの場合は空の配列を返す
      }).catch(error => {
        return []
      })

もしFirestoreからのデータ取得の際にエラーが発生した場合には、「return []」で空の配列をresultに格納する設定にしています。

resultを返してpropsとして扱えるようにする
return {datas: result}

先程までの処理でデータがdata配列に入り、その配列がresultという変数に格納されました。(エラーの場合は空のdata配列がresult変数に格納されています。)

そのresultをpropsである「datas」に渡します。(名前は任意です)
こうすることでコンポーネント内でpropsとして格納したFirebaseのデータを扱うことができるようになるというわけですね。

render関数で取得したデータを描画

  render() {
    const firestoreDatas = this.props.datas
    return (
        <div>
            <h3>Firestoreのデータ一覧</h3>
            <div>
              <ul>
              {firestoreDatas.map(fanPage =>
                <li key={fanPage.id}>
                    <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}>
                      <a>{fanPage.artistName}</a>
                    </Link>
                </li>
              )}
              </ul>
            </div>
        </div>
        );
        }
    }

次にrender関数で取得したデータを描画しています。こちらも細かく分けてみていきますね。

propsを定数に格納
const datas = this.props.datas

先程getInitialPropsで返されたpropsは「this.props.datas」という呼び方で参照する(props値を使用する)ことができます。

しかし名称として長いため、「datas」という定数に置き換えています。

map関数でpropsを展開
              {datas.map(fanPage =>
    // 省略
              )}

map関数は配列の中身を展開することができるメソッドです。
先程dataをconsole.logで確認した際を思い出してください。

[ { id: 'JNTwv1d5W0g1yXQr667j',
    artistName: 'test1',
    body: 'test1',
    category: 'singer',
    monthlyFee: '1000',
    pageName: 'test1' },
  { id: 'aW749fTXcm99meV7x2WB',
    artistName: 'test2',
    body: 'test2',
    category: 'calligrapher',
    monthlyFee: '2000',
    pageName: 'test2' } ]

このようになっていました。この中身をオブジェクトごとに1つずつ展開することができるのです。

今はオブジェクト型データが2つだけしか入っていないためあまりメリットは感じられませんが、大量のデータを処理する際にはとても便利な関数です。

そして「datas.map(fanPage」と書くことによって、datasの中のオブジェクトを1つずつ「fanPage」という変数に格納します。

foreachでいうところの「foreach 配列 as 変数」という書き方に似ていますね。

mapで展開されたデータから値を参照
                <li key={fanPage.id}>
                    <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}>
                      <a>{fanPage.artistName}</a>
                    </Link>
                </li>

上記のような書き方でリストレンダリングすることができます。

 { id: 'JNTwv1d5W0g1yXQr667j',
    artistName: 'test1',
    body: 'test1',
    category: 'singer',
    monthlyFee: '1000',
    pageName: 'test1' },

上記のようにオブジェクトにデータが入っているため、「fanPage.id」「fanPage.body」などといった書き方で個別にデータを参照することができるのです。

Linkの書き方について
 <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}>

Linkの書き方にも注目しましょう。

hrefでは実際にpages/p/[detailid].jsというファイルにアクセスするということが明示されています。しかし実際にユーザーがURLで確認できるのは「/p/JNTwv1d5W0g1yXQr667j」などといった独自URLです。

このような書き方をすることによって動的なページを作成することができるようになっています。

動的なページの作成

次にURLごとにページの中身が切り替わる動的なページを作成していきます。

pages/p/[detail].jsを作成しましょう。

import { db } from '../../lib/db';
import React from 'react';

export default class Detail extends React.Component {
    static async getInitialProps({query}) {
        let result = await 
            db.collection("fanPages")
            .doc(query.detail)
            .get()
            .then(function(doc) {
                if (doc.exists) {
                    return doc.data();
                } else {
                    console.log('not exists');
                }
            }).catch(error => {
                console.log(error)
                return []
            })
          return {detail: result}
        }

      render() {
          const detail = this.props.detail;
        return (
            <React.Fragment>
                <div>
                    <h1>{detail.artistName}</h1>
                    <p>
                        {detail.body}
                    </p>
                    <ul>
                        <li>{detail.category}</li>
                        <li>{detail.monthlyFee}</li>
                        <li>{detail.pageName}</li>
                    </ul>
                </div>
            </React.Fragment>
          );
      }
}

これで以下のような画面が出てきたのではないでしょうか。
f:id:nekorokkekun:20190925114118p:plain

こちらも分けて解説をしていきます。

getInitialPropsでURLクエリパラメータから得たIDを元にデータを取得

まずはURLクエリパラメータから渡ってきたデータIDを元に、Firestoreから任意のデータを取得します。

先程扱ったgetInitialPropsと異なる点が何点かありますね。

getInitialPropsには引数がある

実はgetInitialPropsには動的なページ作成に必要な情報を引数として自動的に生成してくれる機能があります。

    static async getInitialProps({query}) {

上記の例では「query」という引数を受け取っています。
このqueryをconsole.logで見てみると…

{ detail: 'JNTwv1d5W0g1yXQr667j' }

と出力されました。
URLクエリパラメータでも使用した一意のデータIDですね。
このデータIDだけにアクセスしたければ「query.detail」と書いてあげればOKですね。

他にもgetInitialPropsには様々な引数があります。

Next.jsのドキュメントには以下のような記述がありました。

nitialProps receives a context object with the following properties:

pathname - path section of URL
query - query string section of URL parsed as an object
asPath - String of the actual path (including the query) shows in the browser
req - HTTP request object (server only)
res - HTTP response object (server only)
err - Error object if any error is encountered during the rendering
Documentation - Getting Started | Next.js

console.logで中を見てみるとかなり膨大な量のデータを引数で受け取れるということがわかりますので、こちらも検証してみてくださいね。

ドキュメントを指定したデータの取得
            db.collection("fanPages")
            .doc(query.detail)
            .get()
            .then(function(doc) {
                if (doc.exists) {
                    return doc.data();
                } else {
                    console.log('not exists');
                }

先程はコレクションのみの指定でしたが、さらにデータIDを引数にしてドキュメントを指定することができます。

MySQLで言う所の

Select * from fanPages where id = 1;

の「where」以降の条件付けとでも言えばいいでしょうか。

whereがなければテーブル(コレクション)全てのデータを取得することとなり、それは先程行った全件取得と同じですね。

そしてドキュメントの指定をする場合には「.doc(query.detail)」といった書き方が必要です。引数の「query.detail」には先程説明した通り、一意のデータIDが入っています。

後は先ほど説明したgetInitialPropsとほとんど変わりませんので割愛します。

render関数で描画

        const detail = this.props.detail;
        return (
            <React.Fragment>
                <div>
                    <h1>{detail.artistName}</h1>
                    <p>
                        {detail.body}
                    </p>
                    <ul>
                        <li>{detail.category}</li>
                        <li>{detail.monthlyFee}</li>
                        <li>{detail.pageName}</li>
                    </ul>
                </div>

propsで受けとったデータは再度、定数に格納して「detail.artistName」といった形で参照可能です。

これでFirestoreから任意のデータを取得して描画することができました。お疲れ様でした!