投稿日
Reactでバリデーションありのフォームを実装する
こんにちは。西日本テクノロジー&イノベーション室の藤田です。
技術的に強くなりたいと言っている新卒3年目です。
最近フィットボクシングを始めました。日々フックを打つ手が力強くなっていくのを実感しています。
この前書きは以下の内容とはまったく関係ありません。
はじめに
React + Railsでサービス開発をしていて、その時に実装したバリデーションありのフォームについてまとめます。
フォームでやっていることは、以下の3点です。
- APIから初期値を取得して各フォームに設定する
- ユーザーが入力した値に対してバリデーションを行い、結果次第でエラーメッセージを表示させる
- 入力項目をpostする
やっていることは特殊なところもなく、フォームとしては一般的な内容かと思います。本記事では、ソースコードを参照しながらやっていることを一通り説明します。
実装
ここでは、名前と電話番号を入力するフォームを実装します。
今回、あらかじめ外部SNSログインをしてもらっている想定で、初期値はその外部SNSから取得します。また、名前も電話番号もどちらも必須項目として、空欄であればエラーメッセージを表示させ、バリデーションがOKであればpostします。
フォーム画面を開いてから情報を登録するまでの一連の流れに沿って、ソースコードを参照しながらポイントを説明します。
流れは以下の通りです。
state
の初期値設定- APIから初期値を取得する
- ユーザーが必要な項目を入力する
- postする前に入力項目をチェックする
- 入力項目をpostする
state
の初期値設定
入力項目とそれに対応するエラーメッセージ、処理の状態を管理します。
エラーメッセージは、項目に対して複数エラーメッセージがある場合を考慮して配列で持ちます。また、全てのエラーメッセージをクライアント側で用意せず、APIから受け取ったエラーメッセージを出力できるように構造を合わせています。
this.state = {
name: "",
phone: "",
errors: {
name: [],
phone: [],
_global: []
},
processing: false
};
}
APIから初期値を取得する
componentDidMount()
でAPIから初期値を取得してstate
に設定します。
外部SNSにログインしておらず、初期値が取得できなかった場合は401エラーが返ってきます。401エラーが返ってきた場合は、正規のフローでユーザー登録をしていないものとみなして、LP(ランディングページ)に遷移します。
componentDidMount() {
this.fetchUserData();
}
async fetchUserData() {
try {
const resp = await axios.get("/api/users/sign_up");
const json = resp.data;
this.setState({ ...json });
} catch (e) {
if (e.response.status === 401) {
window.location.href = "LPのURL";
}
}
}
ユーザーが必要な項目を入力する
見やすいようにユーザー名の入力フォームのみ抽出しています。
入力フォームにはAnt Design MobileのInputItemを使っています。ユーザーが登録画面を開いて項目に入力している時点ではまだバリデーションは行っていませんが、もし不正な値と判断された場合errors.name
に設定されているエラーメッセージが入力フォームの直下に赤字で表示されるようにしています。バリデーションは入力項目をpostするときに行います。
また、ユーザーが入力フォームを更新する度、当該項目に関するエラーメッセージを消しています。エラーメッセージが出力され、入力値を修正している間ずっとエラーメッセージが出続けることを避けるためです。
const { name, phone, processing, errors } = this.state;
return (
<div className="signup">
<List>
<InputForm placeholder="ユーザー名を入力してください" onChange={name =>
this.setState({
name,
errors: { ...errors, name: [] }
})
}
value={name} maxLength="20" itemname="ユーザー名" message={errors.name} />
const InputForm = (props) => {
return (
<React.Fragment>
<InputItem {...props}>{props.itemname}</InputItem>
<ErrorMessage message={props.message} />
</React.Fragment>
);
};
const ErrorMessage = (props) => {
const errorstyles = {
color: "#fc0101"
};
const messages = props.message
return (
<React.Fragment>
{messages !== null &&
messages.map(message =>
<p className="error" style={errorstyles} key={message}>
{message}
</p>
)}
</React.Fragment>
);
}
postする前に入力項目をチェックする
ユーザーが入力し終わって、登録ボタンを押したタイミングでバリデーションをかけます。
まずエラーメッセージをリセットし、state
のprocessing
をtrue
に切り替えます。
processing
がtrue
であれば登録ボタンを非活性にしてアイコンをloading
に変更しておきます。ここではAnt Design MobileのButtonとIconを使用しています。
async onSignUpButtonClick() {
const { name, phone } = this.state;
const { history } = this.props;
this.setState({
errors: {
name: [],
phone: [],
_global: []
},
processing: true
});
<Button icon={processing ? "loading" : "check-circle-o"} inline disabled={processing} style={{ color: "#f24d99" }} onClick={this.onSignUpButtonClick.bind(this)}>
登録
</Button>
続いて、必須項目の入力チェックをします。もしname
が""
かnull
の場合は、エラーメッセージをnameErrors
に設定してreturn
します。すると、前項で説明したように、入力フォーム直下にエラーメッセージが表示されます。
const nameErrors = validateRequired(name, "ユーザ名を入力してください");
const phoneErrors = validateRequired(phone, "電話番号を入力してください");
if (nameErrors || phoneErrors) {
this.setState({
errors: { name: nameErrors, phone: phoneErrors, _global: [] },
processing: false
});
return;
}
function validateRequired (property, message){
const error = property === "" || property === null ? [message] : null;
return error;
}
入力項目をpostする
前項に引き続き、onSignUpButtonClick()
内の処理です。
バリデーションの結果がOKであれば、入力項目をpostし、その結果によって処理を分岐させます。
200が返ってきたら、次のページへ遷移させます。
401エラーが返ってきたら、正規のフローでユーザー登録をしていないものとみなしてLPに遷移します。
その他のエラーが起きたら、APIから返されたエラーメッセージをsetState
します。
try {
await axios.post("/api/users", { name, phone });
this.setState({
processing: false
});
history.push("次のページ");
} catch (e) {
if (e.response.status === 401) {
window.location.href = "LPのURL";
return;
}
const errors = e.response.data;
const _global = ["システムでエラーが起きました"];
this.setState({
errors: { ...errors, _global },
processing: false
});
}
コード全体
以上の説明をふまえて、フォームのコード全体は次の通りです。
import React from "react";
import { WhiteSpace, Button, InputItem, List } from "antd-mobile";
import axios from 'axios';
const InputForm = (props) => {
return (
<React.Fragment>
<InputItem {...props}>{props.itemname}</InputItem>
<ErrorMessage message={props.message} />
</React.Fragment>
);
};
const ErrorMessage = (props) => {
const errorstyles = {
color: "#fc0101"
};
const messages = props.message
return (
<React.Fragment>
{messages !== null &&
messages.map(message =>
<p className="error" style={errorstyles} key={message}>
{message}
</p>
)}
</React.Fragment>
);
}
function validateRequired (property, message){
const error = property === "" || property === null ? [message] : null;
return error;
}
export default class SignUpRegistrationPage extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
phone: "",
errors: {
name: [],
phone: [],
_global: []
},
processing: false
};
}
componentDidMount() {
this.fetchUserData();
}
async fetchUserData() {
try {
const resp = await axios.get("/api/users/sign_up");
const json = resp.data;
this.setState({ ...json });
} catch (e) {
if (e.response.status === 401) {
window.location.href = "LPのURL";
}
}
}
async onSignUpButtonClick() {
const { name, phone } = this.state;
const { history } = this.props;
this.setState({
errors: {
name: [],
phone: [],
_global: []
},
processing: true
});
const nameErrors = validateRequired(name, "ユーザ名を入力してください");
const phoneErrors = validateRequired(phone, "電話番号を入力してください");
if (nameErrors || phoneErrors) {
this.setState({
errors: { name: nameErrors, phone: phoneErrors, _global: [] },
processing: false
});
return;
}
try {
await axios.post("/api/users", { name, phone });
this.setState({
processing: false
});
history.push("次のページ");
} catch (e) {
if (e.response.status === 401) {
window.location.href = "LPのURL";
return;
}
const errors = e.response.data;
const _global = ["システムでエラーが起きました"];
this.setState({
errors: { ...errors, _global },
processing: false
});
}
}
render() {
const { name, phone, processing, errors } = this.state;
return (
<div className="signup">
<List>
<InputForm placeholder="ユーザー名を入力してください" onChange={name =>
this.setState({
name,
errors: { ...errors, name: [] }
})
}
value={name} maxLength="20" itemname="ユーザー名" message={errors.name} />
<WhiteSpace size="xl" />
<InputForm type="number" placeholder="0912345678" maxLength="20" onChange={phone =>
this.setState({
phone,
errors: { ...errors, phone: [] }
})
}
value={phone} itemname="電話番号" message={errors.phone} labelNumber={7} />
<WhiteSpace size="sm" />
</List>
<WhiteSpace size="xl" />
<Button icon={processing ? "loading" : "check-circle-o"} inline disabled={processing} style={{ color: "#f24d99" }} onClick={this.onSignUpButtonClick.bind(this)}>
登録
</Button>
<ErrorMessage message={errors._global} />
<WhiteSpace />
</div>
);
}
}
おわりに
Reactでバリデーションありのフォームを実装する方法をまとめました。
エラーメッセージを配列で持つところ、APIからエラーメッセージを受け取れるように構造を揃えているところがポイントです。
バリデーションの種類を増やしてエラーメッセージが増えた場合もこの実装であれば拡張が容易です。
本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。