【Node.js + TypeScript + SendGrid】メール送信方法と、「TypeError: Load failed」で苦しんだ話

Next.jsを用いたWebアプリケーション内で、APIを用いてメールを送信する方法をまとめました!

流れとしては、

  1. フォームから情報を入力
  2. 「入力情報を確認」ボタンを押す
  3. 入力情報確認ページに遷移
  4. 「送信」ボタンを押す
  5. 「お問い合わせありがとうございました」のページに遷移し、メールも送信する

こんな感じです。

メール送信は SendGrid を利用します。

こんな人におすすめ!
  • Next.js × TypeScript でお問い合わせフォームを設置したい方
  • ある程度 React や Next.js、TypeScriptの知識がある方

また余談ですが、メール送信を実装している最中に「TypeError: Load failed」というエラーが発生し、約1日苦しめられたので自戒として後半に原因を載せております。

目次

SendGridのアカウントを作成し、APIキーを取得する

まずはSendGridにアカウントを登録し、APIキーを取得する必要があります。

「SendGrid APIキー取得」などとググれば取得方法がでてくるかと思います。

こちらのサイトなどおすすめです。

各ページ及び内部処理を作り込む

本記事では、あくまでメールを送信するまでの機能を実装するのが目的ですので、レイアウトやデザインの作り込みは必要最低限に留めております。ご了承ください。決してデザインセンスがないわけではございません。決して。

必要なモジュールのインストール

npm install recoil --save
npm install @sendgrid/mail --save

pages/index.tsx

import Link from 'next/link'

export default function Home() {
  return (
    <>
      <Link href="/contact/">contact</Link>
    </>
  )
}

実際にアクセスするとこんな感じです。感想は「質素」です。

types/inputTypeForm.tsx

type InputFormType = {
  name: string;
  email: string;
  tel: string;
  content: string;
};

export default InputFormType;

ここでフォーム入力内容の種類とその型を定義します。

今回のユーザーの入力内容としては、

・名前
・メールアドレス
・電話番号
・本文

を想定しております。

他にも入力内容が必要な場合は、適宜ここで定義してあげましょう!

states/atoms/inputAtom.tsx

import { atom } from 'recoil'
import InputFormType from '../../types/inputFormType'

export const inputState = atom<InputFormType>({
  key: 'input', 
  default: {
    name: '', 
    email: '', 
    tel: '', 
    content: '',
  }
});

Recoilを用いて状態保持は「atom」を使用して行います。

「types/inputTypeForm.tsx」で定義したデータ型を用いてatomオブジェクトを定義しています。

  1. key にはユニークとなる値を指定してあげる。
  2. default には初期値を設定できる。

pages/__app.tsx

import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { useReducer } from 'react'
import { RecoilRoot } from 'recoil'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
    </RecoilRoot>
  )
}

先ほど定義したatomでの状態管理を行うためには RecoilRoot でラップしてあげる必要があります。

pages/contact.tsx

import { useRecoilState } from 'recoil'
import { inputState } from '../states/atoms/inputAtom'
import { useForm } from "react-hook-form"
import InputFormType from '../types/inputFormType'
import Router from 'next/router'


type SubmitProps = {
  onSubmit?: () => void;
}

export default function Home() {
  const [input, setInput] = useRecoilState(inputState)

  // useFormを使用してフォーム情報のオブジェクトを作成する。「defaultValues」には、Recoilから取得した初期値を設定している。指定することで「確認画面」から戻ってきた時も値を保持できる。
  const { register, handleSubmit, formState: { errors } } = useForm<InputFormType>({
    defaultValues: {
      name: input.name, 
      email: input.email, 
      tel: input.tel, 
      content: input.content, 
    }
  });

  // フォームのsubmitを実行する時の処理。入力情報を「Recoil」に設定する。
  const onSubmit = handleSubmit((data: InputFormType) => {
    setInput((currentInput) => ({
      ...currentInput, 
      ...{
        name: data.name, 
        email: data.email, 
        tel: data.tel, 
        content: data.content, 
      }
    }));
    Router.push('/confirm');    // /confirm に遷移
  });

  return (
    <>
      <section className="section-form">
        <div className="wrapper-header">
          <h2>お問い合わせ</h2>
        </div>
        <form className="contact-form">
          <ul className="input-from-list">
            <li>
              <p>お名前(必須)</p>
              <input 
                type="text" 
                {...register("name", {required: '※入力が必須の項目です'})} 
                required
              />
              {errors.name?.message && <div>{errors.name.message}</div>}
            </li>
            <li>
              <p>メールアドレス(必須)</p>
              <input 
                type="email" 
                {...register("email", {required: '※入力が必須の項目です'})} 
                required 
              />
              {errors.email?.message && <div>{errors.email.message}</div>}
            </li>
            <li>
              <p>お電話番号(必須)</p>
              <input 
                type="tel" 
                {...register("tel", {required: '※入力が必須の項目です'})} 
                required 
              />
              {errors.tel?.message && <div>{errors.tel.message}</div>}
            </li>
            <li>
              <p>お問合せ内容</p>
              <textarea {...register("content")} />
            </li>
          </ul>
          <SubmitBtn onSubmit={onSubmit} />
        </form>
      </section>
    </>
  )
}

const SubmitBtn = (props: SubmitProps) => {
  return (
    <>
      <button type="submit" className="submitBtn" onClick={props.onSubmit}>
        <div>
          <h3>入力内容を確認する</h3>
        </div>
      </button>
    </>
  )
}

画面上の表示はこんな感じです。

※CSSはこちら

styles/globals.css
/* 共通 */

* {
	box-sizing: border-box;
}

.wrapper-header {
	text-align: center;
	margin: 50px;
}

/* お問い合わせフォーム */

.contact-form {
	background-color: rgb(228, 228, 229);
	width: 600px;
	margin: 3rem auto;
	padding: 30px;
	box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 3px 0px, rgba(27, 31, 35, 0.15) 0px 0px 0px 1px;
}

.input-from-list {
	list-style: none;
}

.input-from-list li input {
	width: 90%;
	height: 28px;
	font-size: 16px;
}

textarea {
	width: 90%;
	height: 250px;
	resize: none;
	font-size: 16px;
}

.submitBtn {
	margin: 20px 0;
	position: relative;
	left: 50%;
	transform: translateX(-50%);
}

長いので要点を絞って解説していきます。

const [input, setInput] = useRecoilState(inputState)

useRecoilState に、先ほど作成して atom を指定することで、その値を使用することができます。

const { register, handleSubmit, formState: { errors } } = useForm<InputFormType>({
  defaultValues: {
    name: input.name, 
    email: input.email, 
    tel: input.tel, 
    content: input.content, 
  }
});

続いて、useForm を使用してフォーム情報のオブジェクトを作成しています。

defaultValues に、Recoilから取得した入力情報を初期値として設定しておくことで、「確認画面」から戻ってきても値を保持しておくことができます。

const onSubmit = handleSubmit((data: InputFormType) => {
  setInput((currentInput) => ({
    ...currentInput, 
    ...{
      name: data.name, 
      email: data.email, 
      tel: data.tel, 
      content: data.content, 
    }
  }));
  Router.push('/confirm');    // /confirm に遷移
});

ここでは「入力内容を確認する」ボタンをしたときの処理を記述しています。

ユーザーが入力した情報を Recoil に保持し、その後 /confirm に遷移します。

pages/confirm.tsx

import { useRecoilValue } from "recoil"
import { inputState } from "../states/atoms/inputAtom"
import Router from "next/router"
import Link from "next/link"

type SubmitProps = {
  onSubmit?: () => Promise<void>;
}

export default function Home() {
  const input = useRecoilValue(inputState);

  const onSubmit = async () => {
    try {
      const res = await fetch('/api/send_mail', {
        method: 'POST', 
        headers: {
          Accept: 'application/json, text/plain, */*',
          'Content-Type': 'application/json',
        }, 
        body: JSON.stringify(input), 
      });
      console.log('res: ', res);
      alert('お問い合わせ送信されました。');
      Router.push('/thanks')
    } catch (err) {
      console.error('Fetch error: ', err);
      alert(err)
    }
  }

  return (
    <>
      <section>
        <div className="wrapperHeader">
          <h2>入力内容確認</h2>
        </div>
        <div>
          <div>
            <ul>
              <li>
                <h3>お名前</h3>
                <p>{input.name}</p>
              </li>
              <li>
                <h3>メールアドレス</h3>
                <p>{input.email}</p>
              </li>
              <li>
                <h3>電話番号</h3>
                <p>{input.tel}</p>
              </li>
              <li>
                <h3>お問合せ内容</h3>
                <p>{input.content}</p>
              </li>
            </ul>
            <div>
              <p>上記の内容で間違いなければ[ この内容で送信する ]ボタンを押してください。</p>
            </div>
            <div>
              <Link href="/contact/"><< 入力内容を修正する</Link>  
            </div>
            <SubmitBtn onSubmit={onSubmit} />
          </div>
        </div>
      </section>
    </>
  )
}

const SubmitBtn = (props: SubmitProps) => {
  return (
    <>
      <button type="submit" className="submitBtn" onClick={props.onSubmit}>
        <div>
          <h3>送信する</h3>
        </div>
      </button>
    </>
  )
}

こちらも長いですね。

contact/ の画面で Recoil に保持した値を取り出し表示させることで、入力内容を確認することができます。

また、「送信する」ボタンを押した後のメールを送信する処理を、onSubmit関数にて実装しています。

実際にAPIを叩いてメールを送信する処理は、後述の pages/api/send_mail.tsx にて記述します。

余談ですが、64行目の <SubmitBtn>コンポーネントを formタグで括ってしまうと「TypeError: Load failed」というエラーが発生してしまいます!

このような具合ですね。

<>
  <section>
    ...省略

    <form>
      ...省略
      <SubmitBtn onSubmit={onSubmit} />
    </form>
  </section>
</>

/contact にて使用していた formタグをそのままにしておくと進まなくなってしまうので、ぶち消してあげましょう。

私はこの原因が分からずめちゃくちゃ時間をとられました、、、。

pages/api/send_mail.tsx

import type { NextApiRequest, NextApiResponse } from 'next'
import sgMail from '@sendgrid/mail'
import { MailDataRequired } from '@sendgrid/mail'
import { EmailData } from '@sendgrid/helpers/classes/email-address'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<any>
) {
  sgMail.setApiKey('<取得したAPIキー>');
  const msg: MailDataRequired = {
    to: '<送信先メールアドレス>', 
    from: '<送信元メールアドレス>' as EmailData, 
    subject: '確認メッセージ', 
    text: `${req.body.name}さん からのお問い合わせです`, 
    html: `
      <strong>${req.body.name}さんからの問い合わせです</strong>
      <p>お名前: ${req.body.name}</p>
      <p>email: ${req.body.email}</p>
      <p>tel: ${req.body.tel}</p>
      <p>お問い合わせ内容:\n${req.body.content}</p>
    `, 
  };

  console.log('req.body: ', req.body);

  try {
    await sgMail.send(msg);
    res.status(200).json(msg);
  } catch (err: any) {
    console.error(err);
    res.status(500).json(err);
  }
}

ここでは、APIを叩いて実際にメールを送信する処理を記述しています。

sgMail.setApiKey('<取得したAPIキー>');

ここには実際に取得した send grid のAPIキーを入力したください。

またメールの本文の記述内容等は、お好みの文章に適宜編集してください!

pages/thanks.tsx

export default function Home() {
  return (
    <>
      <div>
        <h2>お問い合わせありがとうございました</h2>
      </div>
    </>
  )
}

最後はメールを送信した後の画面です。

無事メールを送信できているか確認してみましょう!

まとめ

本記事では「Node.js」「TypeScript」「SendGrid」を用いたお問い合わせフォームの作成方法を紹介させていただきました!

主題がデザインなどではなく、あくまでメールを送信するまでの最低限の処理内容ですので、その辺はご自身の好みのフォーム内容・デザインに修正していただけたら幸いです。

もし何か不明点・間違っている点などございましたらお気軽にコメントください!

参考にさせていただいた記事

案件、ありますか?

「メインの仕事があるけれど、週1、2日だけできる仕事ないかな、、、」

「ある程度スキルが身に付いてはきたけど、そのスキルを活用できる場が欲しい」

なんて悩みが以前はありました。

自分で仕事を探しに行くのも大事ですが、蛇の道は蛇。その道の人に頼むことで、自分だけでは見つからないような案件に携わることができます。

IT PRO パートナーズでは、簡単に無料でアカウントを登録でき、さらにはエージェントさんに希望の働き方・案件の種類を提示することでお仕事を紹介してくれます!

登録自体も非常に簡単で、「エージェントさんとの面談を希望する」という欄にチェックをするだけで、エージェントさんから直接連絡をいただくことができます。

驚くほど簡単で正直拍子抜けしてしまいました笑

もしお仕事探しに困っておりましたら、一度登録し案件を眺めてみることをおすすめします!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

勤めていた設計会社を退社し、フリーランスとして活動しています
また、趣味で主にpyhonを用いて機械学習を行なっています!
現在競艇の予測モデルの開発中です。

コメント

コメントする

目次