こんにちは、広野です。
MUI をご存知でしょうか。マテリアルデザインという考え方に基づいてデザインされた React 用 WEB パーツ集です。これを使うと、デザインセンスが無くても見栄えの良いサイトを開発できます。
![](https://mui.com/static/social-previews/home-preview.jpg)
本記事では、MUI を使ったメール送信フォーム画面を作ってみます。
実は、React 初心者にとってフォーム開発はなにげにハードルが高いです。state や handleChange の概念が理解できていないと、書いたコードが全然動かず、あきらめてしまう方をこれまで何人も見てきました。そのため、本記事では handleChange について突っ込んで解説します。
さらに!
フロントエンドだけ開発してもメールフォームは成り立ちませんので、バックエンドでメール送信する仕組みも AWS CloudFormation で作れるテンプレートを用意しました。フロントエンドからバックエンドまで一気通貫したメール送信フォーム開発ができますので、ご活用頂ければと思います。
つくりたいもの
こんな感じです。
細かい作り込みは後述しますが、表に見える WEB パーツとしては以下の3つを使用します。それぞれ、リンク先にはサンプルデザインとサンプルコードがいくつも置いてあるので、それを参考に、一般的な HTML タグの代わりに MUI が提供するコードを React コードの中に埋め込んでいくことになります。
- テキスト入力フォーム
![](https://mui.com/static/logo.png)
- ボタン
![](https://mui.com/static/logo.png)
- アイコン
![](https://mui.com/static/logo.png)
環境
- React 18
- MUI 5.8.4
MUI インストール手順
MUI の公式ドキュメント通りなのですが、React が入っている状態で以下のコマンドを実行します。(npm を使っています)
アイコンを使う場合は、アイコンのドキュメントに書いてある追加モジュール @mui/icons-material もインストールします。
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
![](https://mui.com/static/logo.png)
サンプル React コード
この画面を1つの関数コンポーネントにしてみました。
- Feedback.js
import React, { useState } from 'react'; import Box from '@mui/material/Box'; import FormControl from '@mui/material/FormControl'; import TextField from '@mui/material/TextField'; import Button from '@mui/material/Button'; import SendIcon from '@mui/icons-material/Send'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import axios from 'axios'; const Feedback = () => { //メールアドレスフォーマットチェック用正規表現定義 const regexpEmail = /^[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/; //見た目の設定 const theme = createTheme({ typography: { fontFamily: "inherit", button: { textTransform: "none", fontFamily: "inherit" } } }); const textfieldStyles = { backgroundColor: "#ffffff", fontFamily: "inherit" }; const textlabelStyles = { fontFamily: "inherit" }; //state定義 const [values, setValues] = useState( { fbname: "", fbemail: "", fbmessage: "", isSubmitted: false } ); //文字入力の度にstate更新 const handleChange = (e) => { const target = e.target; const value = target.type === "checkbox" ? target.checked : target.value; const name = target.name; setValues({ ...values, [name]: value }); }; //送信ボタンクリック後の処理 const handleSubmit = async () => { await axios.post( "https://xxxx.xxxx.xxxx", //メール送信APIのエンドポイントを記入 { "subj": "Web からのフィードバック", "name": values.fbname, "email": values.fbemail, "body": values.fbmessage } ); setValues({ isSubmitted: true }); }; //画面表示内容 return values.isSubmitted ? ( <section> {/* 送信済みフラグが true であれば 送信しました画面 を表示 */} <h2>フィードバック</h2> <p>フィードバックを送信しました。<br />貴重なご意見、ありがとうございました。</p> </section> ) : ( <section> {/* 送信済みフラグが false であれば 入力画面 を表示 */} <h2>フィードバック</h2> <Box component="form" noValidate autoComplete="off"> <ThemeProvider theme={theme}> <FormControl fullWidth> <TextField name="fbname" id="fbname" label="お名前" placeholder="お名前" variant="filled" margin="dense" size="small" InputProps={{ style: textfieldStyles }} InputLabelProps={{ style: textlabelStyles }} value={values.fbname} onChange={handleChange} /> <TextField name="fbemail" id="fbemail" label="メールアドレス" placeholder="メールアドレス" variant="filled" margin="dense" size="small" InputProps={{ style: textfieldStyles }} InputLabelProps={{ style: textlabelStyles }} value={values.fbemail} onChange={handleChange} error={((regexpEmail.test(values.email) && (values.email)) || !(values.email)) ? false : true} helperText={((regexpEmail.test(values.fbemail) && (values.fbemail)) || !(values.fbemail)) ? null : "Incorrect Email address format."} /> <TextField name="fbmessage" id="fbmessage" label="メッセージ" multiline minRows={3} placeholder="メッセージ" variant="filled" margin="dense" size="small" InputProps={{ style: textfieldStyles }} InputLabelProps={{ style: textlabelStyles }} value={values.fbmessage} onChange={handleChange} /> </FormControl> <br /> <br /> <Button variant="contained" size="large" endIcon={<SendIcon />} disabled={(values.fbname && regexpEmail.test(values.fbemail) && values.fbmessage) ? false : true} onClick={handleSubmit} > フィードバックを送る </Button> </ThemeProvider> </Box> </section> ); }; export default Feedback;
コード解説
- <TextField> タグで各入力用テキストフィールドをつくります。MUI 独特の属性があります。
メールアドレスについては入力内容のチェックが入っており、一般的なメールアドレスの形式かどうかをチェックします。もしメールアドレスの形式でないデータの場合は、エラーメッセージが表示されます。 - <Button> タグでボタンをつくります。こちらも MUI 独特の属性があります。endIcon= という属性を使用し、SendIcon という紙飛行機アイコンを文字の右側に配置しています。
フィードバックを送るボタンは、名前、メッセージ、および正しいメールアドレスが入力されていることを条件にボタンが有効になります。ボタンが無効なときは色がグレーになっています。
- 各入力項目は、value= で values というステートに格納されているデータを常に表示するようになっています。
ユーザがフォームのテキストフィールドに何か入力すると、1 文字ごとの変更の度に onChange= に指定されている handleChange 関数を呼び出します。handleChange 関数は、入力されたデータを values ステートに格納する動きをします。それにより、ユーザが入力したデータが handleChange 関数を経由して、常に最新の入力データがテキストフィールドに表示されることになります。当たり前のようで当たり前でない、でも理に適った不思議な動きをします。
このサンプルコードで記載されている handleChange 関数はチェックボックスのフォームでも使用できるようになっており、非常に汎用的にできています。他でも使い回しが可能です。 - フィードバックを送るボタンを押すと、onClick= に定義されている handleSubmit 関数を呼び出します。handleSubmit 関数は、その時点で values ステートに格納されているユーザの入力データをメール送信 API に送ります。そのときに使用するモジュールは axios を使用します。
- このように、フォームの入力データをフォームの外側にある変数(ステート)で完全に管理し、送る等のアクションを起こすときにはステートをそのまま送ってあげればよい、というつくりは「Controlled」と呼ばれます。このようなフォームを、Controlled form、管理されたフォームと呼びます。
- handleSubmit 関数は、もう 1 つ仕事をします。メール送信済みフラグ values.isSubmitted を true に変えます。そうすると、return values.isSubmitted ? ( で記述されている if 条件分岐に従い、入力フォーム画面が「フィードバックを送信しました」画面に変わります。
- そのほか、MUI のモジュールとして以下を使用しています。全て見た目を整えるためのものです。
<Box> : 四角の表示枠をつくる。このフィードバック欄全体が Box で囲まれている。
<FormControl> : フォームの設定をする。ここでは、このタグで囲まれた <TextField> の横幅を <Box> 内でフルサイズにするため使用。これがないと、自動で <TextField> の横幅がバラバラになってしまい、見た目がかっこ悪くなる。
<ThemeProvider> : 見た目を変更するために使用。特にフォームのパーツで使われている文字のフォントを変更しようと思ったら、このモジュールがないと変更できなかった。
管理されたフォームについて
管理されたフォームについて、突っ込んで解説します。
handleChange 関数とは
コード解説の中で、handleChange という関数が登場しました。これは、フォームで使われている入力値の管理をするために重要な働きをする関数で、以下のような動きをします。
実は、1文字変更する毎にこのサイクルが回っています。
コード効率化のため、入力値を管理する state を配列にして複数の入力値を1つの state にまとめ、handleChange 関数も1つにしてしまうのがテクニックです。(本記事のコードがそうなっています)
管理されたフォームにするメリット
React でフォームを作るときに、なぜわざわざこのような回りくどい処理をさせるのでしょうか。
それは、以下のようなメリットがあるからです。
画面表示値と、React管理化の変数 (state) を常時同期させることにより、
- 他の処理が、その処理実行時点で state に格納されている値を参照し自由に使うことができる。(メール送信のボタンはまさにこれを活用)
- state の値に変更があったことをトリガーに、他の処理を呼び出すことができる。(useEffect を使用、本記事では使用例なし)
メール送信 API
メール送信画面(フロントエンド)からメールを送信するためには、メール送信 API(バックエンド)が必要です。ここでは AWS で構築するサンプルを紹介します。
以下のアーキテクチャになります。何の変哲もありません。
一応、誰でも送信できないように、Amazon API Gateway に CORS の設定はドメイン指定で入れておきます。実装は以下の AWS CloudFormation テンプレートで行います。パラメータに、アクセスを許可したいドメイン名、サブドメイン名を入れて下さい。
Amazon API Gateway はフロントエンドからメール件名、お名前、メールアドレス、メッセージのデータを受け取り、AWS Lambda 関数がメールテンプレートにデータを埋め込んで Amazon SES にメール送信命令を出します。
Amazon SES のサンドボックスは解除済み、送信元メールアドレスが Amazon SES の信頼済みアイデンティティに登録されていることが前提となります。送信元/送信先メールアドレスは AWS CloudFormation テンプレートのパラメータで指定します。
この AWS CloudFormation テンプレートで作成された Amazon API Gateway (HTTP API) のエンドポイントを、上述 React コード内で axios での API 呼出先として埋め込みます。エンドポイント URL は出力タブに表示されるようになっています。
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates an API Gateway, a Lambda function and relevant IAM roles for sending emails via SES. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System sub name of example. Default: xxxxx MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. Default: example.com MaxLength: 40 MinLength: 5 AllowedPattern: "[^\\s@]+\\.[^\\s@]+" SubDomainName: Type: String Description: Sub domain name for URL. Default: xxxxx MaxLength: 20 MinLength: 1 FeedbackSenderEmail: Type: String Description: E-mail address that sends feedback emails. Default: sender@example.com MaxLength: 100 MinLength: 5 AllowedPattern: "[^\\s@]+@[^\\s@]+\\.[^\\s@]+" FeedbackReceiverEmail: Type: String Description: E-mail address that receives feedback emails. Default: receiver@example.com MaxLength: 100 MinLength: 5 AllowedPattern: "[^\\s@]+@[^\\s@]+\\.[^\\s@]+" Resources: # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaSendFeedbackByEmail: Type: AWS::Lambda::Function Properties: FunctionName: !Sub example-SendFeedbackByEmail-${SystemName} Description: !Sub Lambda Function to send user's feedback from web site for example-${SystemName} Runtime: python3.9 Timeout: 3 MemorySize: 128 Role: !GetAtt LambdaSesInvocationRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub example-${SystemName} Code: ZipFile: !Sub | import json import datetime import boto3 def datetimeconverter(o): if isinstance(o, datetime.datetime): return str(o) def lambda_handler(event, context): try: # Define Variables ses = boto3.client('ses',region_name='${AWS::Region}') d = json.loads(event['body']) print({"ReceivedData": d}) RECEIVERS = [] RECEIVERS.append('${FeedbackReceiverEmail}') SENDER = '${FeedbackSenderEmail}' # Send Email res = ses.send_email( Destination={ 'ToAddresses': RECEIVERS }, Message={ 'Body': { 'Text': { 'Charset': 'UTF-8', 'Data': 'NAME: ' + d['name'] + '\nMAILADDRESS: ' + d['email'] + '\n\nMESSAGE:\n' + d['body'] } }, 'Subject': { 'Charset': 'UTF-8', 'Data': d['subj'] + ' (${SubDomainName}.${DomainName})' } }, Source=SENDER, ReplyToAddresses=[d['email']] ) except Exception as e: print(e) return { 'isBase64Encoded': False, 'statusCode': 200, 'body': str(e) } else: return { 'isBase64Encoded': False, 'statusCode': 200, 'body': json.dumps(res, indent=4, default=datetimeconverter) } # ------------------------------------------------------------# # API Gateway # ------------------------------------------------------------# HttpApiSendFeedbackByEmail: Type: AWS::ApiGatewayV2::Api Properties: Name: !Sub example-SendFeedbackByEmail-${SystemName} Description: !Sub HTTP API to send user's feedback from web site for example-${SystemName} ProtocolType: HTTP CorsConfiguration: AllowCredentials: false AllowHeaders: - "*" AllowMethods: - POST - OPTIONS AllowOrigins: - !Sub https://${SubDomainName}.${DomainName} ExposeHeaders: - "*" MaxAge: 600 DisableExecuteApiEndpoint: false Tags: Cost: !Sub example-${SystemName} HttpApiIntegrationSendFeedbackByEmail: Type: AWS::ApiGatewayV2::Integration Properties: ApiId: !Ref HttpApiSendFeedbackByEmail IntegrationMethod: POST IntegrationType: AWS_PROXY IntegrationUri: !GetAtt LambdaSendFeedbackByEmail.Arn CredentialsArn: !GetAtt ApiGatewayLambdaInvocationRole.Arn PayloadFormatVersion: 2.0 TimeoutInMillis: 5000 HttpApiRouteSendFeedbackByEmail: Type: AWS::ApiGatewayV2::Route Properties: ApiId: !Ref HttpApiSendFeedbackByEmail RouteKey: POST /SendFeedbackByEmail Target: !Sub integrations/${HttpApiIntegrationSendFeedbackByEmail} HttpApiStageSendFeedbackByEmail: Type: AWS::ApiGatewayV2::Stage Properties: ApiId: !Ref HttpApiSendFeedbackByEmail AutoDeploy: true StageName: $default # ------------------------------------------------------------# # Lambda SES Invocation Role (IAM) # ------------------------------------------------------------# LambdaSesInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub example-SesAccess-${SystemName} Description: This role allows Lambda functions to access SES. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSESFullAccess - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess # ------------------------------------------------------------# # API Gateway Lambda Invocation Role (IAM) # ------------------------------------------------------------# ApiGatewayLambdaInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub example-ApiGatewayLambdaInvocation-${SystemName} Description: This role allows API Gateways to invoke Lambda functions. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaRole - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: # API Gateway APIGatewayEndpointSendFeedbackByEmail: Value: !Join - "" - - !GetAtt HttpApiSendFeedbackByEmail.ApiEndpoint - /SendFeedbackByEmail
まとめ
いかがでしたでしょうか?
WEB メールフォームを作成するにあたり、必要なフロントエンド画面を React と MUI で作り込み、AWS でバックエンドを作って組み合わせる構成を超ざっくりと、一部だけ突っ込んで紹介いたしました。MUI は複雑な CSS 無しでかっこいい画面が簡単につくれるので、非常にお勧めです。
本記事が皆様のお役に立てれば幸いです。