クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】Realtime Database、Firestore(&スプレッドシート)のデータをFetch APIで読み込む(Next.js環境)

Reactの続きです。以前、React環境からFirebaseのRealtime DatabaseをFetch APIで読み込みました。今回はNext.js環境からRealtime Database、Firestore、ついでにスプレッドシート(Google Sheets API)のデータを読み込んでみます。完成品をデプロイしたVercel環境上でも問題なく動きました♪それではいきましょう!

【目次】

※参考:前回記事
【React】フックとRealtime DatabaseをFetch APIで連携(React + Firebase環境) - クモのようにコツコツと

※参考:Reactを習得するためにやったことまとめ
qiita.com

作ったもの

f:id:idr_zz:20220113114429j:plain

プレビュー

※参考:FirebaseのDB読み込みテスト

コード

※参考:GitHub - ryo-i/firebase-test

ベースとなった環境(Nextスターターキット)

以前、作成したNextスターターキットをベースにする。Next.js + TypeScript + CSS in JS環境。

プレビュー

※参考:Nextアプリスターターキット

コード

※参考:GitHub - ryo-i/next-app-started

詳細はこちらを参照

※参考:【React】ReactでWebアプリを作るシリーズまとめ(随時更新) - Qiita

Fetch APIでデータを読み込む(Innerコンポーネント)

InnerコンポーネントでFetch APIでデータを読み込み、ページに表示する

※参考:firebase-test/Inner.tsx at main · ryo-i/firebase-test · GitHub

import部分

import React, { useState, useEffect, useContext }  from 'react';
import { Context } from '../pages/index';
  • ReactからuseState, useEffect, useContextをimport
  • indexページからContextをimport

APIのurlをindexページのContextから読み込む形にした

※参考:フック API リファレンス – React
※参考:【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと


Innerコンポーネントの本体

// Component
function Inner() {
// 中略
}

以下、Innerコンポーネントの中身


フック設定

  // Hooks
  const [error, setError] = useState(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [members, setMembers] = useState([])
  const context = useContext(Context);
  const url = context.url;
  console.log('url', url);

エラーerror、読み込み中isLoaded、データmembers、コンテキストcontext、コンテキスト内のURLurl


Fetch APIでurlからデータを読み込む

  useEffect(() => {
    async function getJsonData (url) {
      try {
        const res = await fetch(url);
        const resJson = await res.json();
        setIsLoaded(true);
        const getMembers = resJson;
        console.log('getMembers', getMembers);
        setMembers(getMembers);
      } catch(error) {
        setIsLoaded(true);
        setError(error);
        console.log('err', error);
      }
    };

    getJsonData(url);
  }, []);
  • useEffect()でページ読み込み時に1回実行する
  • getJsonData()関数の引数はurlでそこからAPIのデータを読み込む
  • try(成功時):フックisLoadedをtrueにし、フックmembersにレスポンスのデータ内容をセット
  • catch(失敗時):フックisLoadedをtrueにし、フックerrorにエラー内容をセット

ReactでAPIを叩く方法

※参考:AJAX と API – React
※参考:【React】fetchメソッドでREST APIを叩く方法 - omathin blog

Fetch APIをthenからasync/awaitの書き換え

※参考:【JS】async/await構文で書いたFetch APIからJSONデータを読み込む - クモのようにコツコツと


JSX部分

  // JSX
  if (error) {
    return <p>エラー: {error.message}</p>;
  } else if (!isLoaded) {
    return <p>読み込み中...</p>;
  } else {
    return (
      <>
        <dl>
          <dt>ビートルズ:</dt>
          <dd>
            <ul>
              {members.map(member => (
                <li key={member.id}>
                  {member.name}{member.part})
                </li>
              ))}
            </ul>
          </dd>
        </dl>
      </>
    );
  }
  • エラーだったらエラー内容を表示
  • 読み込み中だったら「読み込み中」と表示
  • それ以外はデータの内容を表示

最後にInnerコンポーネントをエクスポート

export default Inner;

JSONファイルのデータを読み込む

まずNextスターターキット内にJSONファイルを置いて、そのデータをFetch APIで読み込んでみる。

member.json作成

「public/data」フォルダの中にmember.jsonを作成

[
    {
        "id":1,
        "name":"ジョン・レノン",
        "part":"ギター"
    },
    {
        "id":2,
        "name":"ポール・マッカートニー",
        "part":"ベース"
    },
    {
        "id":3,
        "name":"ジョージ・ハリスン",
        "part":"ギター"
    },
    {
        "id":4,
        "name":"リンゴ・スター",
        "part":"ドラム"
    }
]

内容はビートルズの名前とパートのみ。

※参考:firebase-test/member.json at main · ryo-i/firebase-test · GitHub

indexページでJSONファイルのurlからデータを読み込む(index.tsx)

各種データの読み込みはindexページで行う

※参考:firebase-test/index.tsx at main · ryo-i/firebase-test · GitHub


先ほどのInnerコンポーネントをimportする

import Inner from '../components/Inner';

先ほどのjsonファイルのurlを設定

const url = {
  json: {
    url: 'data/member.json'
  },
  // 中略
};

コンテキストのexport設定(初期値はurl.jsonにしておく)

export const Context: React.Context<{url: string;}> = createContext(url.json);

Homeコンポーネントの中でContextからJSONのurlをInnerコンポーネントに渡す

function Home() {
  return (
    <>
        // 中略
        <section>
          <h2>JSONファイルから読み込み</h2>
          <Context.Provider value={url.json} >
            <Inner />
          </Context.Provider>
        </section>
        // 中略
    </>
  )
}

結果

InnerコンポーネントのFetch APIでmember.jsonからのレスポンスを取得して f:id:idr_zz:20220116080609j:plain

ビートルズメンバーが表示された! f:id:idr_zz:20220116075848j:plain

スプレッドシートのデータをFetch APIで読み込む

次にスプレッドシートのデータを読み込んでみる。

元のスプレッドシートデータ

スプレッドシートでビートルズメンバーの表を作成 f:id:idr_zz:20220116080739j:plain

Google Sheets APIでスプレッドシートを叩く

Google Sheets APIを使うとスプシのデータのAPIを叩いてJSONのレスポンスを取得できる!

※参考:Sheets API  |  Google Developers
※参考:【最新版】Google SpreadSheetのデータをJSONで取得する手順 | マコブログ

いろいろな取得方法があるが今回はシンプルな「values」にしてみる。

※参考:Google Sheets APIでセルの値を読み込む方法 - Qiita
※参考:Googleスプレッドシートをプログラムから操作 - Qiita


スプシのURLからIDを取得、GCPでGoogle API Keyを設定すると下記のurlを開くと

https://sheets.googleapis.com/v4/spreadsheets/(スプシのID)/values/beatles?key=(APIキー)

JSONデータが表示された! f:id:idr_zz:20220116081825j:plain

スプシから帰ってくるデータは二次元配列形式になっている。配列の1つ目がキー名になっている。

配列を連想配列に変換

二次元配列形式のままだとInnerコンポーネントでデータをうまく取得できない。これをキーと値の連想配列の形に変換したい。

下記のGASでスプレッドシートの配列の1行目をキー名にして連想配列化している方法がとても参考になった!

※参考:【GAS】スプレッドシートのデータからオブジェクトを作る方法|もりさんのプログラミング手帳


新たに「Inner2」コンポーネントを作成して「Inner」コンポーネントの内容を改造した。

※参考:firebase-test/Inner2.tsx at main · ryo-i/firebase-test · GitHub

urlからレスポンスのjsonを取得するとこまでは一緒

        const res = await fetch(url);
        const resJson = await res.json();
        setIsLoaded(true);
        const getMembers = resJson;
        console.log('getMembers', getMembers);

配列の1つ目からキー名の変数を設定

        const keyId = getMembers.values[0][0];
        const keyName = getMembers.values[0][1];
        const keyPart = getMembers.values[0][2];

新たに空の配列を作り、先ほどのキー名と値の連想配列を入れる

        const resultMembers = [];
        for (var i = 1; i < getMembers.values.length; i++) {
          const thisMember = {};
          thisMember[keyId] = getMembers.values[i][0];
          thisMember[keyName] = getMembers.values[i][1];
          thisMember[keyPart] = getMembers.values[i][2];
          resultMembers.push(thisMember);
        }
        console.log('resultMembers', resultMembers);

変数iの初期値を1にすることで2つ目から実行される


多次元配列が連想配列の形式に変換された! f:id:idr_zz:20220116085403j:plain


変換した方の結果をフックmembersに入れる

        setMembers(resultMembers);

APIキーをVercelの環境変数で設定

GitHub上にGoogle API Key付きのurlを載せたところ「GitGuardian」からセキュリティについて警告メールが届いた。。

※参考:GitGuardian なるものからメールが来た件 – .NET ゆる〜りワーク
※参考:Google APIキーを間違えてパブリックリポジトリに公開するとこうなる | tr`s log

下記の方法にするのが安全そう

  • GitHub上に公開したAPIキーをGCPで削除する
  • GCPで新たなAPIキーを再発行
  • 新たなAPIキーをVercelの環境変数設定

これでAPIキーをGitHubを介さずに取得できる

※参考:Vercelに Firebase Admin SDK の秘密鍵の情報を設定する


Next & Vercelでの環境変数の設定方法

※参考:Basic Features: 環境変数 | Next.js

「Settings」の「Environment Variables」でキーと値を入れて設定する f:id:idr_zz:20220116082759j:plain


環境変数をローカル環境で取得するにはルート直下に「.env.local」ファイルを作成し、その中に環境変数を設定する。変数名の頭にはNEXT_PUBLIC_付ける

NEXT_PUBLIC_GOOGLE_KEY=(APIキー)

※参考:【Node.js/Next.js】環境変数(.env)のチートシート|2022年1月版

Vercelのデプロイ環境では無しでも取得できるがローカルで動作確認したいので付けるものと思っておいたほうがいい。


また、環境変数の変更したあと、Vercel上に変更が反映されなかった。

下記の「View build logs」の「Promote to Production」をクリックする方法で反映された!

※参考:Vercel Serverless Functions で環境変数が読み込まれない

f:id:idr_zz:20220116083457j:plain

Google Sheets APIのurlを設定

index.tsxの変数keyに先ほどの環境変数を設定

const key = process.env.NEXT_PUBLIC_GOOGLE_KEY;

process.env.環境変数名で取得できる


urlオブジェクトのspreadsheetキーにurlを設定

const url = {
  // 中略
  spreadsheet: {
    url: 'https://sheets.googleapis.com/v4/spreadsheets/(スプシのID)/values/beatles?key=' + key
  },
  // 中略
};

APIキー部分は変数keyを組み合わせて生成する


index.tsxでurl.spreadsheetのurlをInner2コンポーネントに渡す

function Home() {
  return (
    <>
        // 中略
        <section>
          <h2>スプレッドシートから読み込み</h2>
          <Context.Provider value={url.spreadsheet} >
            <Inner2 />
          </Context.Provider>
        </section>
        // 中略
    </>
  )
}

結果

Inner2コンポーネントのFetch APIでスプレッドシートからのレスポンスを取得して f:id:idr_zz:20220116085651j:plain

ビートルズメンバーが表示された! f:id:idr_zz:20220116085706j:plain

Realtime DatabaseのデータをFetch APIで読み込む

次にFirebaseのRealtime Databeseのデータを読み込んでみる。

JSONデータのインポート

Realtime DatabeseにはJSONをインポートする機能があるので「member.json」をインポートしてみる f:id:idr_zz:20220116090111j:plain

「member.json」がそのままの形でインポートされた。まったくカン・タン・だ! f:id:idr_zz:20220116090159j:plain


なお、Realtime Databaseは全体が一つのJSONデータのため、データ全体が丸見えになってしまう。キー階層ごとのルール付けで回避でるので、種類が異なるデータの時には設定したい。

※参考:Firebase Realtime Database ルールについて  |  Firebase Documentation
※参考:FirebaseのRealtime Databaseでルールを試してみる - techium

Realtime DatabeseのAPIのurlを設定

Realtime Databeseはurlの末尾に「.json」を追加するとJSON形式のデータを取得できる♪

※参考:[:title]【React】フックとRealtime DatabaseをFetch APIで連携(React + Firebase環境) - クモのようにコツコツと

index.tsxでurlオブジェクトのrealtimeDatabaseキーにRealtime Databeseのurlを設定

const url = {
  // 中略
  realtimeDatabase: {
    url: 'https://fir-test-79045-default-rtdb.asia-southeast1.firebasedatabase.app/.json'
  },
  // 中略
};

realtimeDatabaseのurlをInnerコンポーネントに渡す

function Home() {
  return (
    <>
        // 中略
        <section>
          <h2>Realtime Databaseから読み込み</h2>
          <Context.Provider value={url.realtimeDatabase} >
            <Inner />
          </Context.Provider>
        </section>
        // 中略
    </>
  )
}

結果

InnerコンポーネントのFetch APIでRealtime Databaseからのレスポンスを取得して f:id:idr_zz:20220116091431j:plain

ビートルズメンバーが表示された! f:id:idr_zz:20220116091441j:plain

FirestoreのデータをFetch APIで読み込む

最後にFirebaseのFirestoreのデータを読み込んでみる。

Firestoreとは

Realtime Databaseより後にできたNoSQLデータベース

※参考:Add Firebase to your JavaScript project  |  Firebase Documentation

realtime databaseとfirebaseの比較

  • Firestoreはデータの範囲を細かく制御できる(Realtime Databaseはデータが丸見え)
  • Realtime DatabaseはJSONの一括インポート・エクスポートが強い

どちらも良し悪しがある

※参考:Firebase Realtime DatabaseとFirestoreを使い分けていこうなという話 - KAYAC engineers' blog

firestoreのリージョンは「asia-northeast1」が東京

※参考:Cloud Firestore のロケーション  |  Firebase Documentation

FirestoreのセキュリティルールはRealtime Databaseとは違いJSON形式ではない記述方式。ちゃんと理解する必要がありそうだが、かなりのことができそう。

※参考:Cloud Firestore セキュリティ ルールを使ってみる  |  Firebase Documentation
※参考:知っておいて損はない、Firebaseのセキュリティルールのこと - ログミーTech

データを入力

realtime databaseからfirestoreにデータを移行方法(一括自動変換は難しい)

※参考:Firebase Realtime Database で Cloud Firestore を使用する  |  Firebase Documentation

今回はデータ量も少ないので、コンソール上で手入力した f:id:idr_zz:20220116092739j:plain

データが登録された! f:id:idr_zz:20220116092801j:plain

Firestoreのurlを設定

Firestoreのurlは下記のようになる

https://firestore.googleapis.com/v1/projects/(プロジェクトID)/databases/(default)/documents/(コレクション)/(ドキュメントID)

※参考:Cloud Firestore REST API を使用する  |  Firebase Documentation


例えば「/member/1」だと1のジョン・レノンのみのデータを取得できる f:id:idr_zz:20220116093418j:plain

「/member」だとすべてのドキュメントIDの情報を取得できた。今回はこっちを使う。 f:id:idr_zz:20220116093615j:plain

Realtime Databaseに比べるとデータは結構深い階層にある。

Firebaseのデータを取得

Innerコンポーネントのままだとデータの階層が合わないのでInner3コンポーネントを作って改造する。

fetch()でデータを取得、ここまではこれまでと同じ

 const res = await fetch(url);
        const resJson = await res.json();
        setIsLoaded(true);
        const getMembers = resJson.documents;
        console.log('getMembers', getMembers);

新たなオブジェクトを作って、Firebase内の欲しい階層の値を取得して入れる

        const resultMembers = [];
        for (var i = 0; i < getMembers.length; i++) {
          const thisMember = {
            id: 0,
            name: '',
            part: ''
          };
          thisMember.id = getMembers[i].fields.id.integerValue;
          thisMember.name = getMembers[i].fields.name.stringValue;
          thisMember.part = getMembers[i].fields.part.stringValue;
          resultMembers.push(thisMember);
        }
        console.log('resultMembers', resultMembers);

結果、これまでのJSONと同じ形式に変換できた! f:id:idr_zz:20220116094224j:plain


変換後のデータをフックmembersにセット

        setMembers(resultMembers);

FirestoreのAPIのurlを設定

index.tsxのurlオブジェクトのfirestoreキーにFirestoreのurlを設定

const url = {
  // 中略
  firestore: {
    url: 'https://firestore.googleapis.com/v1/projects/(プロジェクトID)/databases/(default)/documents/member/'
  }
};

firestoreのurlをInner3コンポーネントに渡す

function Home() {
  return (
    <>
        // 中略
        <section>
          <h2>Firesroteから読み込み</h2>
          <Context.Provider value={url.firestore} >
            <Inner3 />
          </Context.Provider>
        </section>
    </>
  )
}

結果

Inner3コンポーネントのFetch APIでFirestoreからのレスポンスを取得して f:id:idr_zz:20220116094719j:plain

ビートルズメンバーが表示された! f:id:idr_zz:20220116094733j:plain

最後に

全体をさわってみた感想

  • Google Sheets API:大量のデータを編集するにはスプシのテーブル形式は使いやすい!
    しかし、レスポンスが連想配列ではないのが不便かも?
  • Realtime Database:ローカルのJSONファイルを一括インポートするのが楽!
    しかし、データ全体が一つのJSONデータのため、ルール付けによってはデータが丸見えになってしまうのが不便かも?
  • Firestore:コレクションごとにデータの範囲を制御できるのが便利そう!
    しかし、JSON一括インポートがなかったり階層が深いのが不便かも?

それぞれにメリットデメリットがある感じがしました。

また、今回は読み込みだけですが、追加、削除、修正などCRUD操作をする場合や、そのセキュリティルール設定も考慮するとまた話が変わっていくかもしれません。

それではまた!


※参考:Reactを習得するためにやったことまとめ
qiita.com