【Next.js】Head内に要素を組み込む(外部スクリプトとか)

Next.jsでHead内にCSSや外部script、titleタグなどを組み込みたかったので調べてみました。


結論としては、

import Head from 'next/head'

をインポートし、

render関数内などに、

                <Head>
                    <script src="https://js.stripe.com/v2/"></script>
                </Head>

こんな感じでHeadタグを作って、組み込みたい要素を入れてあげるだけでOKです!

nextjs-docs-ja.netlify.com

【Firebase Functions】functions@: The engine "node" is incompatible with this module. Expected version "8". Got "10.16.3"エラーが吐き出される

Next.js内でFirebase Functionsを使用するためにyarn serveを行うと以下のようなエラーが。

error functions@: The engine "node" is incompatible with this module. Expected version "8". Got "10.16.3"
error Commands cannot run with an incompatible environment.

Firestoreが求めているnodeのバージョンが8なのですが、
実際に使用されているローカルのnodeのバージョンが10.16.3ですよ、というエラーでした。

nodeを8にダウングレードすると他のプロジェクトに影響が出るかもしれないということでanyenv、nodenvを入れてみたのですが正しく作動しなかったため以下のような対策をしました。

functions/package.json

  "engines": {
    "node": "8"
  },

上記、プロジェクト直下のfunctions/package.jsonを以下のように改変

  "engines": {
    "node": "10"
  },

これでnode10で動くようになりました。

【Laravel5.8】ログアウトのredirect先をカスタマイズしたい

Laravel5.8のmake:authで作成したログアウト機能で、redirect先をカスタマイズしたい際の方法です。


/magotaku/app/Http/Controllers/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; // 追加

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

   // 書き換え
    use AuthenticatesUsers {
        logout as performLogout;
    }

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }
    
   // logoutファンクションを作成
    public function logout(Request $request)
    {
        $this->performLogout($request);
  // redirect先は人それぞれカスタマイズ
        return redirect('/home');
    }
}

以上です。

参考
qiita.com

【Next.js】FirebaseのAuthenticationで登録したユーザー情報を描画する

f:id:nekorokkekun:20190925144437p:plain:w1000
前回の記事では、Next.jsにFirebaseのAuthenticationを使ってユーザー登録機能を追加しました。
nekorokkekun.hatenablog.com

そこで「登録したユーザーの情報を動的に描画するマイページ」を作りましょう。

前提

先ほどもご紹介した前回の記事でログイン機能とHOCの実装まではコピペでも構わないので済ませておいてください。
nekorokkekun.hatenablog.com

ユーザー情報の取得

とは言っても書き方はとても簡単です。

pages/dashboard.js

import React from 'react';
import withAuth from "../src/helpers/withAuth";
// firebaseをimport
import { firebase, auth } from "../src/firebase";

class Dashboard extends React.Component {
    render() {
  //  firebase.auth().currentUserでログインユーザーの情報が取得可能
        const user = firebase.auth().currentUser;
        return(
            <div>
            <h2>アカウント情報</h2>
            <h4>アカウント名</h4>
            {user.displayName}
            <h4>メールアドレス</h4>
            <p>
            {user.email} 
            </p>
            <img src={user.photoURL} />
            </div>
        )
    }

}
export default withAuth(Dashboard);

これで以下のような画面が表示されたはずです。
f:id:nekorokkekun:20190925143829p:plain

firebase.auth().currentUserの中身

firebase.auth().currentUserでログインユーザーの情報を取得しています。

中身はオブジェクト型になっており、先ほどのコードに登場した「displayName」や「email」「photoURL」なども全て登録したユーザーの情報から引き出されています。

他にも色々な情報が入っていますので、必要に応じて取得すると良いでしょう。

【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から任意のデータを取得して描画することができました。お疲れ様でした!

【Next.js】Firebase AuthenticationとHOCを使用してサインイン機能を実装する

f:id:nekorokkekun:20190925000003p:plain:w1000
本記事では、Next.jsプロジェクトにFirebase Authenticationを組み込み、サインイン機能を実装するとともに、HOC(高階コンポーネント)を使用してサインインユーザーのみが閲覧できるページを作成していきます。

Next.jsプロジェクトの準備

まずはNext.jsプロジェクトの準備をしていきましょう。

npx create-next-app next-firebase-auth
cd next-firebase-auth
yarn devhttp://localhost:3000/

http://localhost:3000/にアクセスしてみると以下のような画面が出てくるはずです。
f:id:nekorokkekun:20190924225728p:plain

サインインユーザーのみ見られるコンポーネントの作成

次にサインインユーザーのみが見ることのできるコンポーネントを作成していきましょう。

cd pages
touch dashboard.js

pages/dashboard.js

import React from 'react';
import Nav from '../components/nav';
class Dashboard extends React.Component {
 render() {
   return (
     <div>
          <Nav />
          <h1>Dashboard  Page</h1>
           <p>You can't go into this page if you are not authenticated.</p>
      </div>
     )
   }
}
export default Dashboard;

まだHOCに設定していないためアクセス可能です。

必要となるパッケージやDB接続ファイルの準備

次にFirebaseの接続に必要となるパッケージやDB接続用ファイルの準備をしていきます。

まずはルートディレクトリに.envファイルを作成します。

touch .env

ここにFirebaseでプロジェクトを作成した際に発行された

  • api key
  • auth domain
  • database url
  • project id
  • storage buchet
  • messaging sender id
  • app id

たちを定数として収めていきます。

Firebaseにおけるプロジェクトの作成から上記のキーの発行手順については以下の記事に書かれています。
nekorokkekun.hatenablog.com


取得したキーたちを以下のように.envに書き込んでいきましょう。
.env

FIREBASE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_AUTH_DOMAIN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_DATABASE_URL=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_PROJECT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_STORAGE_BUCKET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_MESSAGING_SENDER_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_APP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


次にnext.config.jsをルートディレクトリ直下に作成します。

touch next.config.js

next.config.js

require("dotenv").config();
const path = require("path");
const Dotenv = require("dotenv-webpack");
module.exports = {
  webpack: config => {
    config.plugins = config.plugins || [];
    config.plugins = [
      ...config.plugins,
      // Read the .env file
      new Dotenv({
        path: path.join(__dirname, ".env"),
        systemvars: true
      })
    ];
    return config;
  }
};

最後にFirebaseへ接続するためのファイルを作成しましょう。

// ルートディレクトリから
mkdir src
cd src
mkdir firebase
cd firebase
touch index.js

src/firebase/index.js

import firebase from "firebase/app";
import "firebase/auth";
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);
}
const auth = firebase.auth();
export { auth, firebase };

これでprocess.env.〇〇(定数名)という形でプロジェクト内の他ファイルから.envで設定した定数名を使用するための下準備ができました!


そして以下の2つのパッケージをインストールすることで.envで設定した定数がprocess.env.〇〇(定数名)といった形でプロジェクト内の他のファイルからアクセス可能となります。

yarn add firebase dotenv
yarn add dotenv-webpack

FirebaseでAuthentication設定

次にFirebaseのコンソール上からAuthenticationの設定をしておきましょう。
先ほど作成したFirebaseのプロジェクトにアクセスします。
そしてサイドメニューの「Authentication」をクリックしましょう。
f:id:nekorokkekun:20190924232207p:plain


画像の順番に従い、
① タブメニューの「ログイン方法」をクリック
② 「有効にする」をオン
③ プロジェクトのサポートメールを設定
という順序で設定を完了させましょう。
f:id:nekorokkekun:20190924232600p:plain

index.jsの書き換え

独自のUIにしていますが、ポイントはfirebaseからauthをimportしているというところです。

import React from "react";
import Link from "next/link";
import Nav from "../components/nav";
import { auth, firebase } from "../src/firebase";
import Head from "next/head"

class Home extends React.Component {
  render() {
    return (
      <div>
        <Head title="Home" />
        <Nav />
        <div className="hero">
          <h1 className="title">
            Welcome to Firebase Authentication in Next.js!
          </h1>
          <p className="description">
            Click on the Dashboard link to visit the dashboard page.
          </p>
          <div className="row">
            <Link href="/dashboard">
              <a className="card">
                <h3>Go to Dashboard&rarr;</h3>
                <p>Visit Dashboard</p>
              </a>
            </Link>
          </div>
        </div>
        <style jsx>{`
          .hero {
            width: 100%;
            color: #333;
          }
          .title {
            margin: 0;
            width: 100%;
            padding-top: 80px;
            line-height: 1.15;
            font-size: 48px;
          }
          .title,
          .description {
            text-align: center;
          }
          .row {
            max-width: 880px;
            margin: 80px auto 40px;
            display: flex;
            flex-direction: row;
            justify-content: space-around;
          }
          .card {
            padding: 18px 18px 24px;
            width: 220px;
            text-align: left;
            text-decoration: none;
            color: #434343;
            border: 1px solid #9b9b9b;
          }
          .card:hover {
            border-color: #067df7;
          }
          .card h3 {
            margin: 0;
            color: #067df7;
            font-size: 18px;
          }
          .card p {
            margin: 0;
            padding: 12px 0 0;
            font-size: 13px;
            color: #333;
          }
        `}</style>
      </div>
    );
  }
}
export default Home;

HOC(高階コンポーネント)の作成

いよいよHOC(高階コンポーネント)を作成します。
高階コンポーネントについて今回は詳しく解説しませんが、「通常のコンポーネントに機能を追加するもの」という認識でOKです。

今回のHOCで追加する機能が「サインイン後に見ることができる」というものですね。

// src直下で
mkdir helpers
cd helpers
touch withAuth.js

src/helpers/withAuth.js

import React from "react";
import router from "next/router";
import { auth } from "../firebase";

const withAuth = Component => {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        status: "LOADING"
      };
    }
    componentDidMount() {
      auth.onAuthStateChanged(authUser => {
        if (authUser) {
          this.setState({
            status: "SIGNED_IN"
          });
        } else {
          router.push("/");
        }
      });
    }
    renderContent() {
      const { status } = this.state;
      if (status == "LOADING") {
        return <h1>Loading ......</h1>;
      } else if (status == "SIGNED_IN") {
        return <Component {...this.props} />;
      }
    }
    render() {
      return <>{this.renderContent()}</>;
    }
  };
};
export default withAuth;

こちらは細かく解説していきましょう。

引数でComponentを受ける

const withAuth = Component => {

後ほど「サインイン後しか見られないコンポーネント」を設定する際に

export default withAuth(コンポーネント名)

という書き方でexportします。

この「コンポーネント名」で指定されたコンポーネントが、withAuthの引数に設定された「Component」を指しています。

ユーザー認証されるまではstateで読み込みが「Loading」

    constructor(props) {
      super(props);
      this.state = {
        status: "LOADING"
      };
    }

stateの状態は基本的に「LOADING」(=見認証の状態)にしておき、
ユーザー認証されて初めてstateが「SIGNED IN」の状態となります。

authUserで認証の可否を判別

    componentDidMount() {
      auth.onAuthStateChanged(authUser => {
        if (authUser) {
          this.setState({
            status: "SIGNED_IN"
          });
        } else {
          router.push("/");
        }
      });
    }

サインインが成功した場合、「authUser」インスタンスの中には、ユーザー情報が入ることになります。そうするとif文でstateのstatusが「SIGNED_ID」になり、コンテンツを見ることができるようになります。

uid(=ユーザーID)やメールアドレスなどが入っているため、サインイン後にも利用できそうですね!

もしユーザーが未認証の場合にはauthUserは空のままのため、elseの方に行き、そのままトップページに遷移することとなります。

statusがLOADINGのままだと、LOADINGという画面が表示されることとなります。それが以下のものですね。

    renderContent() {
      const { status } = this.state;
      if (status == "LOADING") {
        return <h1>Loading ......</h1>;
      } else if (status == "SIGNED_IN") {
        return <Component {...this.props} />;
      }
    }

そしてstatusがSIGNED_INだと、HOCでサインインユーザーのみが見られるComponentを表示できるようになっています。

dashboard.jsへ追記

先ほど作成したdashboard.jsにwithAuth.jsを合体させる形で追記しましょう。

pages/dashboard.js

import React from 'react';
import Nav from '../components/nav';
// 追加
import withAuth from "../src/helpers/withAuth";

class Dashboard extends React.Component {
 render() {
   return (
     <div>
          <Nav />
          <h1>Dashboard  Page</h1>
           <p>You can't go into this page if you are not authenticated.</p>
      </div>
     )
   }
}
// withAuthの引数にdashboard.jsを追加
export default withAuth(Dashboard);

これでサインインユーザーのみがdashboard.jsを見られるような設定となりました!

index.jsに追記

次にサインインなどの処理を行うためのコードをpages/index.jsに追記します。

pages/index.js

import React from "react";
import Link from "next/link";
import Nav from "../components/nav";
import { auth, firebase } from "../src/firebase";
import Head from "next/head"

class Home extends React.Component {
// ここから下を追加
  handleSignIn = () => {
    var provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope("https://www.googleapis.com/auth/contacts.readonly");
    auth
      .signInWithPopup(provider)
      .then(() => {
        alert("You are signed In");
      })
      .catch(err => {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };
  handleSignout = () => {
    auth
      .signOut()
      .then(function() {
        alert("Logout successful");
      })
      .catch(function(error) {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };
// ここまでを追加
  render() {
    return (
      <div>
        <Head title="Home" />
        <Nav />
        <div className="hero">
          <h1 className="title">
            Welcome to Firebase Authentication in Next.js!
          </h1>
          <p className="description">
            Click on the Dashboard link to visit the dashboard page.
          </p>
          <div className="row">
            <Link href="/dashboard">
              <a className="card">
                <h3>Go to Dashboard&rarr;</h3>
                <p>Visit Dashboard</p>
              </a>
            </Link>
   {/* ここから下2行を追加 */}
            <button onClick={this.handleSignIn}>Sign In using google</button> 
            <button onClick={this.handleSignout}>Signout</button>
          </div>
        </div>
        <style jsx>{`
          .hero {
            width: 100%;
            color: #333;
          }
          .title {
            margin: 0;
            width: 100%;
            padding-top: 80px;
            line-height: 1.15;
            font-size: 48px;
          }
          .title,
          .description {
            text-align: center;
          }
          .row {
            max-width: 880px;
            margin: 80px auto 40px;
            display: flex;
            flex-direction: row;
            justify-content: space-around;
          }
          .card {
            padding: 18px 18px 24px;
            width: 220px;
            text-align: left;
            text-decoration: none;
            color: #434343;
            border: 1px solid #9b9b9b;
          }
          .card:hover {
            border-color: #067df7;
          }
          .card h3 {
            margin: 0;
            color: #067df7;
            font-size: 18px;
          }
          .card p {
            margin: 0;
            padding: 12px 0 0;
            font-size: 13px;
            color: #333;
          }
        `}</style>
      </div>
    );
  }
}
export default Home;


こちらも少し解説をします。

以下はサインイン・サインアウトの処理を行うイベントの設定です。

handleSignInでは、ボタンを押すとサインインモーダルウィンドウがポップアップし、サインイン処理ができるという流れになります。

こちらは私たちが作り込む必要はありません。

  handleSignIn = () => {
    var provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope("https://www.googleapis.com/auth/contacts.readonly");
    auth
      .signInWithPopup(provider)
      .then(() => {
        alert("You are signed In");
      })
      .catch(err => {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };

handleSignoutも同じくで、Signout()メソッドを使えば簡単にユーザーを未承認の状態に戻してくれます。

  handleSignout = () => {
    auth
      .signOut()
      .then(function() {
        alert("Logout successful");
      })
      .catch(function(error) {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };

そして、上記2つのイベントを呼び出すのが以下のbuttonですね。

            <button onClick={this.handleSignIn}>Sign In using google</button> 
            <button onClick={this.handleSignout}>Signout</button>

サインインをしてみる

それでは

yarn dev

でローカルサーバを起動して実際にサインインをしてみましょう。

まずはサインインしていない状態でDashboardに移ってみましょう。画面遷移しないですね。

次にサインインボタンをクリックします。

すると以下のようなウィンドウが出るはずです。
f:id:nekorokkekun:20190924235640p:plain


サインインのプロセスを経ると、Dashboardもみることができました。

最後にサインアウトできれば成功です!
お疲れ様でした。

【Next.js】Firestoreのデータを削除する

f:id:nekorokkekun:20190923093024p:plain:w1000
今回はNext.jsからFirebase Cloud Firestoreのデータを削除する方法について解説していきます。

以下の記事を前提にしています。
nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com

コードの全体像

まずは今回のコードの全体像を見ていきましょう。

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

 export default class Posts extends React.Component {
  static async getInitialProps() {
    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) => {
    db.collection('posts')
    .doc(id)
    .delete()
    .then(function() {
      console.log("Document successfully deleted!");
    }).catch(function(error) {
      console.error("Error removing document: ", error);
    });
  }

  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>
    );
  }
}

一意のidを取得する

削除する際に「どのデータを削除するのか」を識別するための一意のidを取得する必要があります。

こちらの記事で述べている通り、Firestoreにデータを登録する際、setで任意のidを設定するか、addで自動的に一意のidを作成しているかと思います。

そのidを特定するためには以下のような記述が必要です。

                {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>
                    )}

map関数で取得したFirestoreの全データを1つずつ展開しています。

中でも以下の記述に着目してほしく、

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

後ほど解説するhandleDeleteというイベントにbindしてpost.idを付随させています。
「post.id」が一意のidということになり、それをイベントにbindさせているということです。

また、onClickにhandleDeleteイベントを指定することによって、クリックすると該当のidのデータを削除する、という運びになります。

削除イベントの作成

  handleDelete = (id) => {
    db.collection('posts')
    .doc(id)
    .delete()
    .then(function() {
      console.log("Document successfully deleted!");
    }).catch(function(error) {
      console.error("Error removing document: ", error);
    });
  }

こちらが指定したidのデータを削除するためのイベントです。
delete関数で指定したidを持つデータが削除されるようになっています。

詳しくは公式ドキュメントを参照してください。

これで該当のデータを削除ボタンで削除できるようになりました!