Reactでformなどの実装をするときにvalidationについてどうしようかなと悩んだのですが、自分の中である程度納得いくものができたので、実装方法についてお話していきます。
最終的な完成は以下のようなものになりました。
またレイアウトを少し調整すると以下のようなイメージになります。
Reactで作るvalidationの方針
今回、Reactでvalidationを作るにあたって以下の方針を立てました。
よく見る形ではありますが、エラーメッセージがリアルタイムに表示されないとUX的にあまりよくありません。
送信ボタンを押してからvalidationで弾かれると、めんどくさいと思うのは僕だけではないでしょう。
Reactのvalidationで調べてみると、大体以下の2つがヒットします。
これらのライブラリを使ってもよかったのですが、
- シンプルなvalidationが欲しかっただけで複雑な条件分岐は不要
- 実装コストやメンテナンスコストはほとんど変わらない
と判断して取り入れるのは見送りました。
validationの実現方法について
おそらく一般的にReactでformを作ることを考えたときに、inputで入力された値をstateで管理するでしょう。
state = {
email: '',
password: '',
}
考え方は同じでエラーメッセージもstateに持つようにします。
state = {
info: {
email: '',
password: '',
...
},
message: {
email: '',
password: '',
...
}
}
このmessage stateにはエラーメッセージが入る(= 不正な値が入力されているか判断可)ようにします。
したがって、
- infoオブジェクトの中のstateが1つでも空の場合
- messageオブジェクトの中のstateが1つでも空でない場合
の際には送信ボタンを押せないようにする処理を書くようにします。
送信ボタンを1度しか押せないように制御する
また、validationを作るにあたって2度リクエストが送れてしまうのは、あまりいい実装とは言えません。
したがって、loading状態を表すstateも1つ用意して送信ボタンがクリックされると同時にloading状態になるような処理も追加していきましょう。
このときのstateは以下になります。
state = {
info: {
email: '',
password: '',
...
},
message: {
email: '',
password: '',
...
},
loading: true,
}
React×validationの実装
では、実際のソースコードを見ていきましょう。
import React, { Component } from 'react';
import Validation from './Validation';
class App extends Component {
state = {
info: {
email: '',
password: ''
},
message: {
email: '',
password: ''
},
loading: false
};
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const key = event.target.name;
const value = event.target.value;
const { info, message } = this.state;
this.setState({
info: { ...info, [key]: value }
});
this.setState({
message: {
...message,
[key]: Validation.formValidate(key, value)
}
});
};
canSubmit = (): boolean => {
const { info, message, loading } = this.state;
const validInfo =
Object.values(info).filter(value => {
return value === '';
}).length === 0;
const validMessage =
Object.values(message).filter(value => {
return value !== '';
}).length === 0;
return validInfo || validMessage || !loading;
};
submit = () => {
this.setState({ loading: true });
// ここに追加処理
this.setState({ loading: false });
};
render() {
const { info, message } = this.state;
return (
<React.Fragment>
<p>
<label>メールアドレス: </label>
<input
type="email"
name="email"
value={info.email}
onChange={event => this.handleChange(event)}
/>
{message.email && (
<p style={{ color: 'red', fontSize: 8 }}>{message.email}</p>
)}
</p>
<p>
<label>パスワード: </label>
<input
type="password"
name="password"
value={info.password}
onChange={event => this.handleChange(event)}
/>
{message.password && (
<p style={{ color: 'red', fontSize: 8 }}>{message.password}</p>
)}
</p>
<p />
<p>
<button disabled={!this.canSubmit()} onClick={() => this.submit()}>
送信
</button>
</p>
</React.Fragment>
);
}
}
export default App;
const emailValidation = (email: string): string => {
if (!email) return 'メールアドレスを入力してください';
const regex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
if (!regex.test(email)) return '正しい形式でメールアドレスを入力してください';
return '';
};
const passwordValidation = (password: string): string => {
if (!password) return 'パスワードを入力してください';
if (password.length < 8) return 'パスワードは8文字以上で入力してください';
return '';
};
class Validation {
static formValidate = (type: string, value: string) => {
switch (type) {
case 'email':
return emailValidation(value);
case 'password':
return passwordValidation(value);
}
};
}
export default Validation;
少し解説を加えていきます。
handleChangeメソッド
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const key = event.target.name;
const value = event.target.value;
const { info, message } = this.state;
this.setState({
info: { ...info, [key]: value }
});
this.setState({
message: {
...message,
[key]: Validation.formValidate(key, value)
}
});
};
stateの下にhandleChangeメソッドを用意しましたが、ここでstateの編集作業をしています。
動作としては、eventが発生する(= inputタグの中身が書き換えられる)際に受け取ったeventから
- inputタグのname属性
- inputタグのvalue属性
を受け取って、stateに反映しています。
canSubmitメソッド
canSubmit = (): boolean => {
const { info, message, loading } = this.state;
const validInfo =
Object.values(info).filter(value => {
return value === '';
}).length === 0;
const validMessage =
Object.values(message).filter(value => {
return value !== '';
}).length === 0;
return validInfo || validMessage || !loading;
};
canSubmitメソッドでは、送信ボタンの disabled 属性をコントロールしており、canSubmit メソッドが true のときにのみボタンが押せるようになっています。
中で実行していることは、infoとmessage stateにそれぞれ、値が入力されている/入力されていないのチェックをしています。
例えば、info stateには、全ての値が空文字以外でないとtrueにならず、最終的にボタンは押せないといった具合です。
このように、info/message の state を管理することで state が増えた場合でも同様の処理で管理できることは大きなメリットかなと思います。
submitメソッド
submit = () => {
this.setState({ loading: true });
// ここに追加処理
this.setState({ loading: false });
};
submitメソッドではボタンが押された際に、送信の処理を行うメソッドです。
ここでは、処理の前後でthis.setState({ loading: [boolean] });
の処理を加えています。
この処理によって、現在、何か通信を行っている状態かの管理をしています。通信を行っている場合は、loading が true になるため、上記の canSubmit() メソッドによりボタンが押せなくなります。
したがって、ボタンを2度連続で押されるといったことも防ぐことが可能であると考えられます。
まとめ
ここまで React で作る validation の処理を実装しましたが、もちろん完璧ではありません。
デベロッパーツールなどでボタン属性は編集できるでしょうし、より複雑な条件分岐をしたいときには素直にライブラリを使ったほうがいいかもしれません。
あくまでも React の validation なので、サーバーサイドでも必ず validation の処理を入れるようにしてください。
最後に余談になりますが、2個目の gif のようなレイアウトを作りたい!という方は material-ui で実現できるので、合わせてご覧ください!