【Next.js】Next.js + React Hooks + Cloud Functions for Firebaseでお問い合わせフォームを実装

はじめに

こんにちは、がんがんです。
Webアプリとかを作っているとお問い合わせフォームが必要になることがあるかと思います。簡単に実装できればいいものの、環境に合わせてお問い合わせフォームの実装は様々なやり方が存在しています。


そこで、今回はCloud Functions for Firebaseを用いてお問い合わせフォームの実装を行っていきたいと思います。

目的

Cloud Functions for Firebaseを利用してお問い合わせフォームを作成していきます。
フロント側はNext.js + Material-ui + React Hooksを使用して実装していきました。
f:id:gangannikki:20200817001922g:plain

Cloud Functions for Firebaseでのメール機能を実装

Functions側の実装を行っていきます。Firebaseのセットアップについてはこちらを参照ください。

環境(Dockerfile)の準備

フロントとFirebaseの環境が異なるため、Dockerを用いた環境をセットアップします。Cloud FunctionsにおけるNode.jsの環境は8・10・12です。


Node.js:8は2021年3月にはシャットダウンされるため、これから使用を検討する方はSparkプラン(無料)→Blazeプラン(従量課金)に変更したうえで使用ください。Functionsのバージョンについては公式ドキュメントを参照ください。
firebase.google.com


今回は実験ということでNode.js:8を使用しています。

Dockerfile
FROM node:8-alpine

WORKDIR /usr/src

RUN npm i -g firebase-tools \
    && rm -rf /var/lib/apt/lists/*
docker-compose
version: '3'
services:
  app:
    build: .
    ports:
      - 9005:9005
    volumes:
      - ./functions:/usr/src
    tty: true
    stdin_open: true
    command: /bin/sh

9005ポートはfirebase login:ci用なので、必要がなければ消しても大丈夫です。
次に、firebase cliを使ってセットアップを行います。基本的には勝手にやってくれるので助かります。

#  on Docker
$  firebase login --no-localhost
$  firebase init functions
#  functions dirが生成後
$  cd functions
#  package.jsonに"nodemailer"を追加しておく
$  yarn install

functionの実装

メールサーバとして使用するgmail送信先のアドレスをfunctions.config()として設定します。

$ firebase functions:config:set gmail.email="メールサーバーとして使うgmail" gmail.password="メールサーバーとして使うgmailのパスワード" admin.email="問い合わせメールの送信先となるページ管理者のアドレス"

実装は下記の通りです。基本的には参考サイトとほぼ同じです。

index.js
const functions = require("firebase-functions");
const nodemailer = require("nodemailer");
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const adminEmail = functions.config().admin.email;

//  メールサーバの設定
const mailTransport = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: gmailEmail,
    pass: gmailPassword
  }
});

//  管理者用のメールテンプレート
const adminContents = data => {
  return `以下内容でホームページよりお問い合わせを受けました。

お名前:
${data.name}

メールアドレス:
${data.email}

内容:
${data.content}
`;
};

exports.sendMail = functions.https.onCall((data, context) => {
  //  メール設定
  let adminMail = {
    from: gmailEmail,
    to: adminEmail,
    subject: "ホームページお問い合わせ",
    text: adminContents(data)
  };

  //  管理者へのメール送信
  mailTransport.sendMail(adminMail, (err, info) => {
    if(err) {
      return console.error(`send failed. ${err}`);
    }
    return console.log("send success.");
  });
});

Firebaseへのデプロイは

$ yarn deploy

で大丈夫です(生成されたpackage.jsonのおかげです)。

Next.js + Material-ui + React Hooksでフロント(お問い合わせフォーム)の実装

フロント側の実装にはNext.js + Material-uiを使用します。stateの管理にはReact Hooksを使用していきます。
Firebaseの初期化は別途行う必要がありますので、参考記事を参照してセットアップしてください(ここでは割愛します)。

contact.js
import React, { useState } from 'react';
import Alert from '@material-ui/lab/Alert';
import Backdrop from '@material-ui/core/Backdrop';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import Container from '@material-ui/core/Container';
import Snackbar from '@material-ui/core/Snackbar';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import firebase from 'firebase/app';
import 'firebase/functions';

const useStyles = makeStyles((theme) => ({
  textField: {
    display: 'flex',
    width: '300px',
  },
  contactForm: {
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    marginTop: '100px',
  },
  backdrop: {
    zIndex: theme.zIndex.drawer + 1,
    color: '#fff',
  },
}));

export default function Contact() {
  const classes = useStyles();
  const [sendData, setSendData] = useState({
    email: '',
    name: '',
    content: ''
  });
  const [loading, setLoading] = useState(false);
  const [snackbarOpen, setSnackBarOpen] = useState(false);
  const [snackbarInfo, setSnackBarInfo] = useState({
    severity: '',
    message: ''
  });

  //  Submit Button
  const handleSubmit = e => {
    e.preventDefault();
    setLoading(true);
    
    let sendMail = firebase.functions().httpsCallable('sendMail');
    sendMail(sendData)
      .then(() => {
        setSnackBarInfo({
          severity: 'success', message: 'お問い合わせありがとうございます。送信完了しました。'
        });
        setSnackBarOpen(true);
        console.log('Successed send mail.');
        setSendData({
          email: '', name: '', content: ''  
        });
      })
      .catch(err => {
        setSnackBarInfo({
          severity: 'error', message: '送信に失敗しました。時間をおいて再度お試しください。'
        });
        setSnackBarOpen(true);
        console.log(err);
      })
      .finally(() => {
        setLoading(false);
      })
  };

  //  Change TextField
  const handleChange = e => {
    setSendData({ ...sendData, [e.target.name]: e.target.value });
  };

  //  Close SnackBar
  const handleSnackBarClose = (event, reason) => {
    if (reason === 'clickaway') {
      return;
    }
    setSnackBarOpen(false);
  }

  return (
    <main>
      <Container maxWidth="sm" className={classes.contactForm}>
        <Typography variant="h4" component="h1" gutterBottom>
          お問い合わせ
        </Typography>
        <TextField 
          name="email" label="メールアドレス" type="mail" required
          className={classes.textField} value={sendData.email} onChange={handleChange}
        />
        <TextField
          name="name" label="お名前" type="text" required
          className={classes.textField} value={sendData.name} onChange={handleChange}
        />
        <TextField name="content" label="お問い合わせ内容" required
          multiline
          rows="8" margin="normal" variant="outlined"
          className={classes.textField} value={sendData.content} onChange={handleChange}
        />
        <Button variant="contained" color="primary" type="submit"
          className={classes.textField} onClick={handleSubmit}
        >
          送信
        </Button>
        <Backdrop className={classes.backdrop} open={loading}>
          <CircularProgress color="inherit" />
        </Backdrop>
        <Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackBarClose}>
          <Alert onClose={handleSnackBarClose} severity={snackbarInfo.severity}>
            {snackbarInfo.message}
          </Alert>
        </Snackbar>
      </Container>
    </main>
  );
}

おわりに

今回はCloud Functions for Firebaseを用いたお問い合わせフォームの実装を行っていきました。比較的簡単に実装できるのは非常に助かります。


現状のお問い合わせフォームはバリデーションなどを行っていません。そのため、React-Hooks-Formあたりを使ったフォームの作成を今後試してみようかと思います。