Next.jsを用いたWebアプリケーション内で、APIを用いてメールを送信する方法をまとめました!
流れとしては、
- フォームから情報を入力
- 「入力情報を確認」ボタンを押す
- 入力情報確認ページに遷移
- 「送信」ボタンを押す
- 「お問い合わせありがとうございました」のページに遷移し、メールも送信する
こんな感じです。
メール送信は 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オブジェクトを定義しています。
- key にはユニークとなる値を指定してあげる。
- 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」を用いたお問い合わせフォームの作成方法を紹介させていただきました!
主題がデザインなどではなく、あくまでメールを送信するまでの最低限の処理内容ですので、その辺はご自身の好みのフォーム内容・デザインに修正していただけたら幸いです。
もし何か不明点・間違っている点などございましたらお気軽にコメントください!
コメント