【Next.js】Firestoreにデータの登録(挿入)をする

f:id:nekorokkekun:20190923084230p:plain:w1000
こちらでは、Next.jsで作成した投稿フォームからFirebaseのCloud FirestoreのDBへデータを登録(インサート)する方法について解説をしていきます。

DB設定などについては以下の記事に詳しく記述してあります。
nekorokkekun.hatenablog.com

FirestoreのDBに接続

Firebase Cloud FirestoreのDBへ接続する方法については以下に詳しく記述してあります。
nekorokkekun.hatenablog.com

データ登録

以下のファイルを作成します。

pages/create-post.js

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

 class CreatePost extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            title: '',
            body: ''
        };
    }
    registerPost = async (evt) => {
        evt.preventDefault();
        db.collection("posts").add({
            title: this.state.title,
            state: this.state.body,
        })
        .then(function() {
            console.log("Document successfully written!");
        })
        .catch(function(error) {
            console.error("Error writing document: ", error);
        });
    }
    onChangeTitle = (evt) => {
        this.setState({ title: evt.target.value });
    }
     onChangeBody = (evt) => {
        this.setState({ body: evt.target.value });
    }
     render() {
        return (
            <div className="post-forms">
                <form onSubmit={this.registerPost}>
                    <label htmlFor="title">タイトル</label><br/>
                    <input name="title" value={this.state.title} onChange={this.onChangeTitle} /><br/>
                    <label htmlFor="body">本文</label><br/>
                    <textarea name="body" value={this.state.body} onChange={this.onChangeBody}></textarea><br/>
                    <button type="submit">投稿</button>
                </form>
                <style jsx>{`
                .post-forms {
                    width: 80%;
                    height: 50%;
                    margin: 0 auto;
                }
                input, textarea {
                    width: 70%;
                }
                textarea {
                    height: 100px;
                }
            `}</style>
            </div>
        )
    }
}
 export default CreatePost;

細かく解説していきます。

DB接続用変数をimport

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

先ほどスキップしたDB接続ですが、DB接続用の変数をこちらでimportしておきます。
詳しくは別記事を参照してください。

stateの設定

    constructor(props) {
        super(props);
        this.state = {
            title: '',
            body: ''
        };
    }

formから入力した値を一時的に保存するためにstateを利用しています。
constructorはライフサイクルの1つで、レンダリングが行われる際に実行されるメソッドです。

また以下のコードを見てみましょう。

    onChangeTitle = (evt) => {
        this.setState({ title: evt.target.value });
    }
     onChangeBody = (evt) => {
        this.setState({ body: evt.target.value });
    }

こちらではtitle、bodyが変更(書き込み)されるとsetStateを使用してstateに保管するためのイベントを設定しています。

データ登録用イベントの設定

    registerPost = async (evt) => {
        evt.preventDefault();
        db.collection("posts").add({
            title: this.state.title,
            state: this.state.body,
        })
        .then(function() {
            console.log("Document successfully written!");
        })
        .catch(function(error) {
            console.error("Error writing document: ", error);
        });
    }

次にデータ登録用イベントを設定していきます。
登録ボタンを押すと、このイベントを発生させるような設定になっています。

着目してほしいのは以下の部分です。

        db.collection("posts").add({
            title: this.state.title,
            state: this.state.body,

先ほどimportしたdb変数に、collectionメソッドを繋げ、postsというコレクションを指定しています。

このようにすることでFirestoreのpostsというコレクション(SQLでいうところのテーブル)を探し、あればその中に、なければ新規作成して、データを登録してくれます。

またaddメソッドを使用し、先ほどstateに保管したtitleとbodyをFirebaseのpostsコレクションの中に登録しています。

データ登録用のメソッドはadd以外にもありますので、以下にそれぞれの特徴と例をまとめておきましょう。

また、詳しくはFirebaseのドキュメントを参照してください。

Firestoreにデータ登録をする際のメソッド

Firestoreにデータ登録をする際のメソッドは基本的に以下の2つです。

  • set
  • add
set

set() を使用してドキュメントを作成する場合、作成するドキュメントの ID を指定する必要があります。次に例を示します。

db.collection("cities").doc("new-city-id").set(data);
add

一方でCloud Firestore が ID を自動的に生成するように設定するのがaddメソッドです。

db.collection("cities").add({
    name: "Tokyo",
    country: "Japan"
})

inputフィールドの作成

                <form onSubmit={this.registerPost}>
                    <label htmlFor="title">タイトル</label><br/>
                    <input name="title" value={this.state.title} onChange={this.onChangeTitle} /><br/>
                    <label htmlFor="body">本文</label><br/>
                    <textarea name="body" value={this.state.body} onChange={this.onChangeBody}></textarea><br/>
                    <button type="submit">投稿</button>
                </form>

最後に上記で作成した各種イベントを設定したinputフィールドを記述しました。
投稿ボタンを押し、Firestoreへの登録に成功すると、Consoleに「Document successfully written!」といったメッセージが流れてくるはずです。

【Next.js】Cloud Firestoreへの接続方法まとめ

f:id:nekorokkekun:20190923081808p:plain:w1000
Next.jsからCloud Firestoreへの接続をするための方法をまとめておきます。

まずやること

忘れがちなDB設定でやるべきことをまとめておきます。

  • .env作成
  • dotenvのインストール
  • next.config.js作成
  • lib/db.js作成

この4つに関しては自分も忘れがちなので、改めてまとめておきます。

.env作成

ルートディレクトリに.envファイルを作成し、Firestoreから取得したDB情報を成形して記述します。

FIREBASE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_AUTH_DOMAIN=xxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_DATABASE_URL=xxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_PROJECT_ID=xxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_STORAGE_BUCKET=xxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_MESSAGING_SENDER_ID=xxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_APP_ID=1:xxxxxxxxxxxxxxxxxxxxxxxx

「""」や「,」はいりません。

dotenvのインストール

.envで変数化したパスワード群を他のファイルで使用する場合、npmやyarnで「dotenv」というモジュールをインストールしておく必要があります。

dotenv - npm

# with npm 
npm install dotenv
 
# or with Yarn 
yarn add dotenv

next.config.js作成

他ファイルで.envの変数を使用するためには、変数名の前に「process.env.」を付ける必要があります。

apiKey: process.env.FIREBASE_API_KEY

そのためお決まりのファイルをルートディレクトリに作成しておきましょう。

next.config.js

const webpack = require('webpack');
require('dotenv').config();

module.exports = {
  webpack: config => {
    const env = Object.keys(process.env).reduce((acc, curr) => {
      acc[`process.env.${curr}`] = JSON.stringify(process.env[curr]);
      return acc;
    }, {});

    config.plugins.push(new webpack.DefinePlugin(env));

    return config;
  }
};

難しい内容ですが「process.env.」を使用できるようにしている、と考えてもらえればOKです。(ちなみにこのファイルがなければ、変数の中身が「undefined」となってしまいます)

lib/db.js作成

最後にDB接続を可能とするためのファイルを作成しましょう。

lib/db.js

import firebase from 'firebase/app'
import 'firebase/firestore'

let db;
try {
  const config = {
        apiKey: process.env.FIREBASE_API_KEY,
        authDomain: process.env.FIREBASE_AUTH_DOMAIN,
        databaseURL: process.env.FIREBASE_DATABASE_URL,
        projectId: process.env.FIREBASE_PROJECT_ID,
        storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
        messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
        appId: process.env.FIREBASE_APP_ID
    };
    if (!firebase.apps.length) {
      firebase.initializeApp(config);
    }
    // Firestoreインスタンスを作成
    db = firebase.firestore();
  } catch (error) {
    console.log(error);
  }

  module.exports = {
    // 本来、initializeAppによる初期化は一度きりのため、
    // 初期化の結果のみを切り出してexportする
    db
  };

これでDB接続ができるようになりました。

具体的な使用例

こんな感じで、db.jsで作成した変数dbを使用できるようになります。

// dbのインポート
import { db } from '../lib/db';
import React from 'react'

 export default class Posts extends React.Component {
  static async getInitialProps() {
    // db.jsのfirebaseのDB接続ファンクション
    // DBのpostsコレクション内を全て取得した結果 = result
    let result = await new Promise((resolve, reject) => {
      db.collection('posts')
      .get()
      .then(snapshot => {
        let data = []
        snapshot.forEach((doc) => {
          data.push(
            Object.assign({
              id: doc.id
            }, doc.data())
          )
        })
        resolve(data)
      }).catch(error => {
        reject([])
      })
    })
    return {posts: result}
  }

  handleDelete = (id) => {
    console.log(id)
  }

  render() {
    const posts = this.props.posts
    return (
        <React.Fragment>
                {posts.map(post =>
                    <div className="post" key={post.id}>
                        <h2>
                            {post.title}
                        </h2>
                        <p>
                            {post.body}
                        </p>
                        <button onClick={this.handleDelete.bind(this, post.id)}>削除</button>
                    </div>
                    )}
            <style jsx>{`
            .post {
                width: 40%;
                border: 1px solid black;
                background-color: gray;
                margin-bottom: 10px;
            }
            `}</style>
        </React.Fragment>
    );
  }
}

【Next.js】Cloud Firestoreに格納されたデータから一意のIDを取得する

Cloud Firestoreに格納されたデータから一意のIDを取得したいという時の方法を解説します。

以下の赤枠の部分ですね!
f:id:nekorokkekun:20190922132511p:plain


すでにFirebaseのDBと接続できているという前提で、以下のような書き方をすれば取得可能です。

pages/Posts.js

// 上記省略
  handleDelete = (id) => {
    console.log(id)
  }

  render() {
    const posts = this.props.posts
    return (
        <React.Fragment>
                {posts.map(post =>
                    <div className="post" key={post.id}>
                        <h2>
                            {post.title}
                        </h2>
                        <p>
                            {post.body}
                        </p>
                        <button onClick={this.handleDelete.bind(this, post.id)}>削除</button>
                    </div>
                    )}
            <style jsx>{`
            .post {
                width: 40%;
                border: 1px solid black;
                background-color: gray;
                margin-bottom: 10px;
            }
            `}</style>
        </React.Fragment>
    );
  }
}

以下のコードがポイントです。

<button onClick={this.handleDelete.bind(this, post.id)}>削除</button>

全体のコードはGithubをご覧ください。
github.com

【Next.js】Cloud FirestoreをNext.jsに組み込みデータ取得

f:id:nekorokkekun:20190921172341p:plain:w1000
こちらではNext.jsのプロジェクトにFirebaseのCloud Firestoreを組み込む方法について解説をしていきます。

Firebaseでプロジェクトの作成〜コレクションの作成

Firebaseにアクセスし、Consoleへ移動しましょう。

f:id:nekorokkekun:20190921093319p:plain

そしてプロジェクトの追加をします。
プロジェクト名は任意で構いません。

プロジェクトが生成されると以下のようなページに遷移します。
f:id:nekorokkekun:20190921093631p:plain

サイドメニューの「開発」から「Database」を選択します。するとCloud Firestoreのページに遷移しました。

「データベースの作成」ボタンをクリックすると、モーダルウィンドウが出てきます。

「テストモードで開始」を選択して「次へ」を押すと、ロケーションを選ぶことができるので東京リージョンの「asia-northeast1」を選びましょう。

「完了」ボタンを押すと、データベースの作成が始まります。

ここまでの手順で以下の画面に遷移します。
f:id:nekorokkekun:20190921095118p:plain

次に「コレクションの開始」をクリックします。
ここからMySQLでいうところのテーブル作成となります。

今回はpostsというドキュメントIDで設定しましょう。

MySQLで例えると以下のようなコレクションを作成します。
f:id:nekorokkekun:20190921095632p:plain

フィールド名とValueを設定します。
f:id:nekorokkekun:20190921095754p:plain

そうすると設定した中身でコレクションができましたね。
f:id:nekorokkekun:20190921100020p:plain

あまり聞きなれない単語が多いかもしれませんが、

  • コレクション = テーブル
  • 一意のID = PRIMARY KEY
  • ドキュメント = カラムとレコード

というイメージです。

Next.js側の設定

次にCloud FirestoreをNext.js側に組み込んでいきます。

組み込むにあたってはNext.jsの新規プロジェクトをnpm init で立ち上げてもらってもいいですし、
同じ環境下で行いたいということであれば、こちらのブランチをgit cloneしてもらっても構いません。


まずはルートディレクトリに「.env」を作成しましょう。
こちらにCloud Firestoreにおける設定情報を記入していくため、「.gitignore」を開き、「.env」を追加しておきます。

.gitignore

node_modules
.next
#追加
.env

Firebaseに戻り、サイドメニューの「Project Overview」を選択。
画像の垢枠の部分をクリックしましょう。
f:id:nekorokkekun:20190921101148p:plain


ウェブアプリの登録画面が出てくるので、こちらも任意で命名します。
その後、スクリプトタグが出てくるため以下の部分をコピーしましょう。

  var firebaseConfig = {
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "pf-fanclub.firebaseapp.com",
    databaseURL: "https://pf-fanclub.firebaseio.com",
    projectId: "pf-fanclub",
    storageBucket: "pf-fanclub.appspot.com",
    messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    appId: "1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

そして、先ほど作成した.envにペーストするのですが、スクリプトを成形する必要があります。

.env

FIREBASE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_AUTH_DOMAIN=pf-fanclub.firebaseapp.com
FIREBASE_DATABASE_URL=https://pf-fanclub.firebaseio.com
FIREBASE_PROJECT_ID=pf-fanclub
FIREBASE_STORAGE_BUCKET=pf-fanclub.appspot.com
FIREBASE_MESSAGING_SENDER_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_APP_ID=1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

「""」や「,」などは削除し、環境変数として命名しましょう。

次にデータベース設定を行っていきます。

環境変数をNode.js下で扱うためのdotenvモジュールをインストールしておきましょう。

npm install --save dotenv

dotenvモジュールについては以下の記事に分かりやすい解説がありましたので引用します。

Node.js のプログラムから環境変数を参照するには process.env を参照します。

ユーザー設定を環境変数で行うようにしているアプリはよくあるのですが、たかが 1 つのアプリのために環境変数を設定するのは嫌だというユーザーは少なからずいます(設定がどこで行われているのかわかりにくいという理由もあります)。

dotenv モジュールを使用すると、カレントディレクトリに置かれた .env ファイルを読み込み、そこに記述されたキー&バリューのペアを process.env 経由で参照できるようになります。

つまり、ユーザはアプリの設定を、従来通り環境変数で行うこともできるし、.env ファイルでも行うことができるようになります。 環境変数を使ってアプリの挙動を変えるような実装をしている場合は、.env ファイルによる設定もサポートしておくと親切です。
環境変数の代わりに .env ファイルを使用する (dotenv) | まくまくNode.jsノート

次にfirebaseをnpm経由でインストールしていきます。

npm install --save firebase

次にルートディレクトリに以下のディレクトリとファイルを作成しましょう。

lib/db.js

import firebase from 'firebase/app'
import 'firebase/firestore'

let db;
try {
  const config = {
        apiKey: process.env.FIREBASE_API_KEY,
        authDomain: process.env.FIREBASE_AUTH_DOMAIN,
        databaseURL: process.env.FIREBASE_DATABASE_URL,
        projectId: process.env.FIREBASE_PROJECT_ID,
        storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
        messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
        appId: process.env.FIREBASE_APP_ID
    };
    firebase.initializeApp(config);
    // Firestoreインスタンスを作成
    db = firebase.firestore();
  } catch (error) {
    console.log(error);
  }

  module.exports = {
    // 本来、initializeAppによる初期化は一度きりのため、
    // 初期化の結果のみを切り出してexportする
    db
  };

Firebaseで表示されたスクリプトを先ほど.envに設定した環境変数に置き換えました。
これで外部からデータベースの設定情報を閲覧されることはありません。

また環境変数の前にprocess.env.を付与することで、dotenvモジュールによってprocess.envを経由した参照になります。

次に以下のファイルをルートディレクトリに作成しましょう。

next.config.js

const webpack = require('webpack');
require('dotenv').config();

module.exports = {
  webpack: config => {
    const env = Object.keys(process.env).reduce((acc, curr) => {
      acc[`process.env.${curr}`] = JSON.stringify(process.env[curr]);
      return acc;
    }, {});

    config.plugins.push(new webpack.DefinePlugin(env));

    return config;
  }
};

もしこの段階で

Firebase: Firebase App named '[DEFAULT]' already exists (app/duplicate-app).

といったエラーが出た場合、色々な原因があり、それに応じた解決策があるようですが、initializeAppを複数回行った覚えがないという場合には以下の方法で解決できるかもしれません。(私の場合はキャッシュが消して開発環境を再構築することで解決できました)
Firebase: Firebase App named &#39;[DEFAULT]&#39; already exists (app/duplicate-app).というエラーに悩まされたときに - Qiita


最後に表示部分を作成します。
pages/Posts.js

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

 export default class Posts extends React.Component {
  static async getInitialProps() {
    // db.jsのfirebaseのDB接続ファンクション
    // DBのpostsコレクション内を全て取得した結果 = result
    let result = await new Promise((resolve, reject) => {
      db.collection('posts')
      .get()
      .then(snapshot => {
        let data = []
        snapshot.forEach((doc) => {
          data.push(
            Object.assign({
              id: doc.id
            }, doc.data())
          )
        })
        resolve(data)
      }).catch(error => {
        reject([])
      })
    })
    return {posts: result}
  }

  handleDelete = (id) => {
    console.log(id)
  }

  render() {
    const posts = this.props.posts
    return (
        <React.Fragment>
                {posts.map(post =>
                    <div className="post" key={post.id}>
                        <h2>
                            {post.title}
                        </h2>
                        <p>
                            {post.body}
                        </p>
                        <button onClick={this.handleDelete.bind(this, post.id)}>削除</button>
                    </div>
                    )}
            <style jsx>{`
            .post {
                width: 40%;
                border: 1px solid black;
                background-color: gray;
                margin-bottom: 10px;
            }
            `}</style>
        </React.Fragment>
    );
  }
}

これでFirebaseのCloud Firestoreに登録されているデータが表示されたはずです!

【Next.js】GCPのデータベースをNext.jsプロジェクトに組み込みたい

こちらではGCPGoogle Cloud Platform)をNext.jsのプロジェクトに組み込む方法を解説していきます。

Next.jsプロジェクトの立ち上げ

まずはNext.jsのプロジェクトを立ち上げましょう。

mkdir myproject
cd myproject
npm init

GCPでデータベースの作成

GCPへアクセスし、[コンソールへ移動]のボタンを押しましょう。
次に

インスタンスを作成]ボタン > MySQL 

と進み、インスタンスの詳細設定を行なっていきます。
f:id:nekorokkekun:20190920081724p:plain

インスタンス名やパスワードは特に決まりがありません。
またリージョンに関しては、東京の場合、「asia-southeast1」となります。
ゾーンは任意で構いません。

GCPシェルからデータベースの作成

次にGCPの画面上部にあるシェルボタンから、シェルプロンプトを起動します。
一番左にあるアイコンですね。
f:id:nekorokkekun:20190920082115p:plain

シェルのインストールが終わると起動できるようになるので、起動しましょう。

シェルに以下のようなメッセージが表示されるので、

Welcome to Cloud Shell! Type "help" to get started.
Your Cloud Platform project in this session is set to {YOUR_GCP_PROJECT}.
Use “gcloud config set project [PROJECT_ID]” to change to a different project.

以下のようにコマンドを打ちます。

gcloud sql connect インスタンス名 --user=root

インスタンス名の部分には、先ほど作成したインスタンスの名前を入れましょう。
次にパスワードが聞かれるので、こちらも先ほど設定したパスワードを入れましょう。

すると以下のようなメッセージが表示され、MySQLの操作が可能となります。

Whitelisting your IP for incoming connection for 5 minutes...done.
Connecting to database with SQL user [root].Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 57
Server version: 5.7.14-google-log (Google)

データベース、テーブルの作成

次にデータベース、テーブルを作成していきます。
これまでMySQLを使用されてきた方は、同様の方法で作成することが可能です。

create database myproject
use myproject
create table posts (id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255), body VARCHAR(255), PRIMARY KEY(id));

これで以下のようなテーブルが作成されました。
f:id:nekorokkekun:20190920083402p:plain

試しにレコードの挿入をしましょう。

insert into posts (title, body) values ("first title", "first body");
insert into posts (title, body) values ("second title", "second body");
select * from posts;

以下のように表示されていればレコードの挿入が成功しています。
f:id:nekorokkekun:20190920083925p:plain

Next.jsプロジェクトにMySQLを接続する準備

まずは必要なパッケージをプロジェクト直下でインストールしていきましょう。

(ここからはGCPのシェルプロンプトではなく、Next.jsのプロジェクトをインストールしたターミナルにコマンドを打ち込んでいきます。)

npm i serverless-mysql sql-template-strings


package.jsonにもbuildコマンドを追記します。(すでにscriptsに書き込まれているコマンドは消さないでください。)

  "scripts": {
   "build": "next build"
  },

Serverless MySQL

GCPのようなサーバーレスMySQLへの接続を管理するためのパッケージ。このパッケージを導入することでデータベースに接続できるようになります。

またスケーリング可能なため、接続数が増えても自動的に拡大してくれる柔軟性があります。
GitHub - jeremydaly/serverless-mysql: A module for managing MySQL connections at SERVERLESS scale

SQL Template Strings

SQLのクエリを簡潔に書くことができるようになるパッケージ。mysql、mysql2、postgres、sequelizeで動作。

以下のような違いが出てくるようです。

const SQL = require('sql-template-strings')

const book = 'harry potter'
const author = 'J. K. Rowling'

// SQL Template Stringsを導入しなかった場合
mysql.query('SELECT author FROM books WHERE name = ? AND author = ?', [book, author])
// SQL Template Stringsを導入した場合
mysql.query(SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`)

またZEIT公式にも以下のように書かれている通り、SQLインジェクションの対策にも繋がるようです。

パラメータ化されたクエリを使用してSQLインジェクションによる攻撃を防ぐために、sql-template-stringsを使用することを強くお勧めします。

GitHub - felixfbecker/node-sql-template-strings: ES6 tagged template strings for prepared SQL statements 📋

データベースへの接続

まずはnowコマンドが使えるようにインストールをしましょう。

npm i -g now

次にデータベースの情報をnowコマンドを使って登録していきましょう。

以下のフォーマットを使用して該当箇所にデータベースの情報を記入していきます。

now secrets add MYSQL_HOST $database-hostname && now secrets add MYSQL_USER $database-username && now secrets add MYSQL_DATABASE $database-name && now secrets add MYSQL_PASSWORD $database-password

私の場合であれば、以下のようになります。

now secrets add MYSQL_HOST "%" && now secrets add MYSQL_USER "root" && now secrets add MYSQL_DATABASE "myproject" && now secrets add MYSQL_PASSWORD "xxxxxxxx"

MYSQL_HOSTとMYSQL_USERに関してはGCPで作成したインスタンスの詳細画面からユーザータブにアクセスすると確認可能です。
f:id:nekorokkekun:20190920093019p:plain

データベースの情報に登録が成功すると、ターミナルに以下のようなメッセージが表示されます。

> Success! Secret mysql_host added (hiramori) [569ms]
> Success! Secret mysql_user added (hiramori) [564ms]
> Success! Secret mysql_database added (hiramori) [600ms]
> Success! Secret mysql_password added (hiramori) [695ms]

これでデータベースの情報が登録され、接続ができるようになったはずです。

データベース接続用のファイルを作成する

先ほど登録した情報をもとに、データベース接続用関数を書き込んでおくためのファイルを作成しましょう。

プロジェクト直下で以下のコマンドを入力します。

mkdir lib
cd lib
touch db.js

lib/db.js

const mysql = require('serverless-mysql')

const db = mysql({
  config: {
    host: process.env.MYSQL_HOST,
    database: process.env.MYSQL_DATABASE,
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD
  }
})

exports.query = async query => {
  try {
    const results = await db.query(query)
    await db.end()
    return results
  } catch (error) {
    return { error }
  }
}

db.jsファイルは次の機能を実行します。
定義されたデータベースの情報を使用して、MySQLデータベースへの接続を作成します。クエリが解決されると接続が閉じられるようにする関数をエクスポートします。

最も重要な行は、await db.end()です。これにより、アプリが利用可能なすべての接続を使い果たすことを防ぎます。

これで、サーバーレス環境に最適な再利用可能なデータベース接続ができました。

APIの作成

次にAPIを作成していきましょう。

まずはpostsテーブルの全レコード取得を行うためのAPI作成です。

mkdir api && mkdir api/posts
cd api/posts
touch index.js

api/posts/index.js

const db = require('../../lib/db')
const escape = require('sql-template-strings')

module.exports = async (req, res) => {
  let page = parseInt(req.query.page) || 1
  const limit = parseInt(req.query.limit) || 9
  if (page < 1) page = 1
  const posts = await db.query(escape`
      SELECT *
      FROM posts
      ORDER BY id
      LIMIT ${(page - 1) * limit}, ${limit}
    `)
  const count = await db.query(escape`
      SELECT COUNT(*)
      AS postsCount
      FROM posts
    `)
  const { postsCount } = count[0]
  const pageCount = Math.ceil(postsCount / limit)
  res.status(200).json({ posts, pageCount, page })
}

index.jsファイルは次の機能を実行します。

  • 要求されたクエリパラメータを解析します
  • クエリパラメータを使用して、必要なpostを決定します
  • データベースから必要なpostのみを要求します
  • データベースを照会して合計レコードを取得します
  • レコード数を使用してページネーションを計算します
  • 取得したpostとページネーションの詳細を応答として送信します

これが、サーバーレス環境でページネーションを正常に使用するために必要なすべてのAPIコードです。


次にpostsテーブルから指定したidのレコードのみを取得するAPIを作成します。

api/postsディレクトリ直下に新たなファイルを作成します。

touch post.js

api/posts/post.js

const db = require('../../lib/db')
const escape = require('sql-template-strings')

module.exports = async (req, res) => {
  const [post] = await db.query(escape`
    SELECT *
    FROM posts
    WHERE id = ${req.query.id}
  `)
  res.status(200).json({ profile })
}

post.jsファイルは次の機能を実行します。

  • リクエストクエリパラメータを解析します
  • クエリパラメータを使用して、データベースから単一のプロファイルを選択します
  • 取得したプロファイルを応答として送信します。

これで、ルートに応じて、すべてのプロファイルまたは単一のプロファイルのいずれかを提供するAPIが作成されました。

取得したデータの表示

次に、それらを表示するアプリインターフェイスを作成する必要があります。
まずは必要なモジュールを取得しましょう。

npm i isomorphic-unfetch next react react-dom
mkdir pages

【React】ReduxのState/Store/Action/Reducer設定まとめ

Vue.jsを使用している際にもVuexやVue Routerなどで、
Stateを全てのコンポーネントに渡すことができるという機能がありましたが、
そのReact版がReact Reduxです。

大まかに、React Reduxを使ったState/Store/Actionの設定まとめをしていきます。

React Reduxの必要性

Reactを使って多重階層のコンポーネントを用いる際、Storeという保管庫からどのコンポーネントからでも簡単にデータを取り出すことができるため便利。

従来のState管理では渡したいデータがあった場合、親コンポーネントから子コンポーネントへ、そして子コンポーネントから孫コンポーネントへ…という煩雑な手間がありました。

下準備

Reactプロジェクト内で、reduxとreact-reduxという2つのパッケージをインストールする。

$ yarn add redux react-redux

また以下のような階層でState/Store/Actionを区分けしておくと便利
📦src
┣ 📂actions
┃ ┗ 📜index.js
┣ 📂components
┃ ┗ 📜App.js
┣ 📂reducers
┃ ┣ 📜count.js
┃ ┗ 📜index.js
┣ 📜index.css
┣ 📜index.js
┗ 📜serviceWorker.js

Action

全てのコンポーネントで共通したアクションを登録しておくためのファイル。
またActionのファイル内で、アクションを定義する関数のことをAction Createrという。

以下は一例
src/actions/index.js

// 複数箇所でINCREMENTなどは使用されるため変数化しておく
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'INCREMENT'

// 下記のINCREMENTとDECREMENTでは書き方が違うが、
// 上はリファクタリングが行われているだけで意味は同じ
// またactionを定義してreturnする関数をアクションクリエイターという
export const increment = () => ({
        type: INCREMENT
})

export const decrement = () => {
    return {
        type: DECREMENT
    }
}

Reducer

アクションが発生した際に、そのアクションに組み込まれているタイプによってどのようにStateを変化させるのかを設定しておくのがReducerです。

全てのReducerをまとめるためのsrc/reducers/index.jsと、個別のReducerであるsrc/reducers/count.jsの2つを用意しました。

src/reducers/index.js

// 全てのReducerをコンバイン(合体)させるためのもの
import { combineReducers } from 'redux'
import count from './count'

export default combineReducers({ count })

実際のReducerの役割は以下のように個別のReducerファイルに分ける
src/reducers/count.js

import { INCREMENT, DECREMENT } from '../actions'

const initialState = { value: 0 }

export default (state = initialState, action) => {
    // eslint-disable-next-line default-case
    switch(action.type) {
        case INCREMENT:
            return { value: state.value + 1 }
        case DECREMENT:
            return { value: state.value - 1 }
        default:
            return state
    }
}

Store

Storeは、アプリ内にある全てのStateを管理するための保管庫というイメージです。
Storeを設定することによってどのコンポーネントからでも値の受け渡しをせずにStateを取り出すことができるようになります。

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// 以下2つをインポート
import { createStore } from 'redux';
import { Provider } from 'react-redux';

import './index.css';
// 上記で作成したReducerをインポート
import reducer from './reducers';

import App from './components/App';
import * as serviceWorker from './serviceWorker';

// アプリ内唯一のstoreを定義。
// またreducerを引数に入れることで
// 全てのアプリケーションのStateがStoreに格納される。
// reducerのcount.js内にあるinitialState = { value: 0 }が渡される。
const store = createStore(reducer);

ReactDOM.render(
 // Providerタグで囲い、store属性に定義したstoreを格納することで、
   // 全てのコンポーネントからStore内に格納されたStateを参照できるようになる
    <Provider store={store}>
    <App />
    </Provider>,
    document.getElementById('root')
    );

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

StateとActionを関連づけるConnect

Stateを管理するためのStore、Actionを設定するためのActionとAction Creater、
そして操作によって適切なActionを呼び出すためのReducersを設定しました。

最後にActionによって適切にStateに処理が反映させるためにconnectを導入します。
src/components/App.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

import { increment, decrement } from '../actions';

class App extends Component {
  render() {
    // stateやactionを格納するためのprops
    const props = this.props

    return (
    <div>
    {/* reducerのcount.js内にあるvalue(initialState)を表示する */}
    { props.value }
    <button onClick={props.increment}>+1</button>
    <button onClick={props.decrement}>-1</button>
    </div>)
  }
}
// mapStateToPropsはStateの情報からコンポーネントで必要な情報を取り出して、
// コンポーネント内のpropsとしてマッピングする機能を持つ
// 引数にはstateを書いて、どういったオブジェクトをpropsとして対応させるのかを定義させる
const mapStateToProps = state => ({ value: state.count.value })

// mapDispatchToPropsはあるアクションが発生した時にReducerにTypeに応じた状態遷移を実行させるための関数
// incrementのためのボタンとdecrementのためのボタンに必要なアクションをdispatch関数の引数に渡す
const mapDispatchToProps = dispatch => ({ 
  increment: () => dispatch(increment()),
  decrement: () => dispatch(decrement())
})

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

これでどのコンポーネントでも同じAction、Stateを使用できるようになりました。

ちなみにmapDispatchToPropsには省略記法があり、以下のように書くことができます。

const mapDispatchToProps = ({ increment, decrement })

たった1行だけですが、処理的に違いはありません。

【React】関数を使ってstateの値を変える

Reactにはpropsとよく似たものにstateという機能があります。
こちらもコンポーネントに対して値を渡すことができる機能ですが、
propsが静的な値(変化しない値)である一方、stateはコンポーネント内で値を変更させることができます。

以下がその使用例です。

import React, { Component } from 'react';

const App = () => (
    <Counter></Counter>
  )

class Counter extends Component {
  constructor(props) {
    super(props) // 親コンポーネントでpropsの初期化処理
    this.state = { count: 0} // stateの値を設定する
  }

  render() {
    return (
    <div>
    { this.state.count } // stateを出力
    </div>)
  }
}
export default App;

このように記述することによって0という値が入ったcountというstateがコンポーネントに渡されます。

上記のコードを使いつつ、stateの値を上下させる関数を設定しました。

import React, { Component } from 'react';

const App = () => (
    <Counter></Counter>
  )

class Counter extends Component {
  constructor(props) {
    super(props)
    this.state = { count: 0}
  }

 // stateの値を上げるための関数
  handlePluss = () => {
    this.setState( { count: this.state.count + 1 } )
  }
 // stateの値を下げるための関数
  handleMinus = () => {
    this.setState({ count: this.state.count - 1 });
  }

  render() {
    return (
    <div>
    { this.state.count }
 // onClickでイベントの設定
    <button onClick={this.handlePluss}>+1</button> 
    <button onClick={this.handleMinus}>-1</button>
    </div>)
  }
}
export default App;

これでstateの値を変えることができるボタンが設置できました。