やりたいこと
下記のような感じ。
ログイン必要ページと不要ページを作成し、ログイン必要ページへのアクセスにはチェックを入れる。
かつ、それをreact-router-domで、わかりやすくきれいに書きたい。
完成イメージ
各ファイルの画面イメージ(画面のあるもの)は下記ような感じ。
準備
雛形の作成とモジュールのインストール
create-react-appを利用します。
作業フォルダ作成後、必要なモジュールをインストールします。
create-react-app auth-test
cd auth-test
yarn add react-router-dom firebase bootstrap reactstrap react-loading-overlay formik yup
各モジュールの役割はおおよそ下記のような感じ。
- react-router-dom : 言わずとしれたルーティング(ページ移動)モジュール
- firebase : 今回はAuthentication機能を利用
- bootstrap reactstrap : bootstrapと強化モジュール
- react-loading-overlay : ローディング画面表示に利用
- formik yup : Formおよびバリデーションに利用
npmの人はnpm install --save react-router-dom firebase bootstrap reactstrap react-loading-overlay formik yup
必要なファイルの生成
必要なファイルを生成しておきます。階層に気をつけながら生成します。
touch .env
cd src
touch Auth.js
touch Firebase.js
mkdir screens
touch screens/Home.js
touch screens/Profile.js
touch screens/SignInOrUp.js
touch screens/SignUp.js
スタイルの適用
bootstrapのCSSが全体で使えるようにindex.jsで読み込んでおきます。
import React from 'react';
import ReactDOM from 'react-dom';
+import 'bootstrap/dist/css/bootstrap.min.css'
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Firebase(を利用できるように)設定
firebaseの各種機能がReactで利用できるよう、認証情報等を適切に設定します。
最上階層に.envを作成し、Firebaseの各種情報を記入します。
REACT_APP_API_KEY=xxxxx
REACT_APP_AUTH_DOMAIN=xxxxx
REACT_APP_DATABASE_URL=xxxxx
REACT_APP_PROJECT_ID=xxxxx
REACT_APP_STORAGE_BUCKET=xxxxx
REACT_APP_MESSAGING_SENDER_ID=xxxxx
REACT_APP_APP_ID=xxxxx
.envは.gitignoreに忘れずに登録しておきましょう。
.envの値を利用してFirebase.jsを設定します。
import firebase from 'firebase/app';
import "firebase/firestore";
import "firebase/auth";
const firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
databaseURL: process.env.REACT_APP_DATABASE_URL,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID
};
firebase.initializeApp(firebaseConfig);
export const db = firebase.firestore();
export default firebase;
実装
では実装していきます。
App.js
まずApp.jsにてルーティング構造を設定します。
今回は<Auth></Auth>で囲ったところが認証エリアになるようにします。
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import firebase from './Firebase';
//screens
import Home from './screens/Home';
import Profile from './screens/Profile';
import SignInOrUp from './screens/SignInOrUp';
import SignUp from './screens/SignUp';
import Auth from './Auth';
class App extends React.Component {
render() {
return (
<Router>
<Switch>
<Route exact path="/signin" component={SignInOrUp} />
<Route exact path="/signup" component={SignUp} />
{/* 以下認証のみ */}
<Auth>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/profile" component={Profile} />
<Route render={() => <p>not found.</p>} />
</Switch>
</Auth>
</Switch>
</Router>
);
}
}
export default App;
Auth.js
認証を司るAuthコンポーネントを作成します。
ログイン状況をチェックし、OKならHomeにNGならSignInOrUpにリダイレクトします。
ログインチェック(onAuthStateChanged)に時間がかかるため、チェック処理が完了済みかどうかをチェックし、チェックが未完了ならローディング画面を表示し、チェック完了を待ちます。
完了すれば、状況に応じて処理を進めます。
import React from 'react';
import { Redirect } from 'react-router-dom';
import firebase from './Firebase';
import LoadingOverlay from 'react-loading-overlay';
class Auth extends React.Component {
state = {
signinCheck: false, //ログインチェックが完了してるか
signedIn: false, //ログインしてるか
}
_isMounted = false; //unmountを判断(エラー防止用)
componentDidMount = () => {
//mountされてる
this._isMounted = true;
//ログインしてるかどうかチェック
firebase.auth().onAuthStateChanged(user => {
if (user) {
//してる
if (this._isMounted) {
this.setState({
signinCheck: true,
signedIn: true,
});
}
} else {
//してない
if (this._isMounted) {
this.setState({
signinCheck: true,
signedIn: false,
});
}
}
})
}
componentWillUnmount = () => {
this._isMounted = false;
}
render() {
//チェックが終わってないなら(ローディング表示)
if (!this.state.signinCheck) {
return (
<LoadingOverlay
active={true}
spinner
text='Loading...'
>
<div style={{ height: '100vh', width: '100vw' }}></div>
</ LoadingOverlay>
);
}
//チェックが終わりかつ
if (this.state.signedIn) {
//サインインしてるとき(そのまま表示)
return this.props.children;
} else {
//してないとき(ログイン画面にリダイレクト)
return <Redirect to="/signin" />
}
}
}
export default Auth;
SignUp.js
登録ユーザーがいないとログインもできないので、まずSignUpから実装してみます。
バリデーション等で少々複雑に見えますが大したことはしていません。
import React from 'react';
// import { Form } from 'react-bootstrap';
import { Button, Form, FormGroup, Label, Input, FormFeedback, Spinner } from 'reactstrap';
import { Link, withRouter } from 'react-router-dom'
import { Formik } from 'formik';
import * as Yup from 'yup';
import firebase from '../Firebase';
class SignUp extends React.Component {
state = {
loading: false, //処理中にボタンにspinner表示する制御用
}
_isMounted = false;
//Submitされたら
handleOnSubmit = (values) => {
//spinner表示開始
if (this._isMounted) this.setState({ loading: true });
//新規登録処理
firebase.auth().createUserWithEmailAndPassword(values.email, values.password)
.then(res => {
//正常終了時
//spinner表示終了
if (this._isMounted) this.setState({ loading: false });
//Homeに移動
this.props.history.push("/"); //history.pushを使うためwithRouterしている
})
.catch(error => {
//異常終了時
if (this._isMounted) this.setState({ loading: false });
alert(error);
});
}
componentDidMount = () => {
this._isMounted = true;
}
componentWillUnmount = () => {
this._isMounted = false;
}
render() {
return (
<div className="container">
<div className="mx-auto" style={{ width: 400, background: '#eee', padding: 20, marginTop: 60 }}>
<p style={{ textAlign: 'center' }}>新規登録</p>
<Formik
initialValues={{ email: '', password: '', tel: '' }}
onSubmit={(values) => this.handleOnSubmit(values)}
validationSchema={Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().required(),
tel: Yup.string().required(),
})}
>
{
({ handleSubmit, handleChange, handleBlur, values, errors, touched }) => (
<Form onSubmit={handleSubmit}>
<FormGroup>
<Label for="name">Email</Label>
<Input
type="email"
name="email"
id="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
invalid={touched.email && errors.email ? true : false}
/>
<FormFeedback>
{errors.email}
</FormFeedback>
</FormGroup>
<FormGroup>
<Label for="password">Password</Label>
<Input
type="password"
name="password"
id="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
invalid={touched.password && errors.password ? true : false}
/>
<FormFeedback>
{errors.password}
</FormFeedback>
</FormGroup>
<FormGroup>
<Label for="tel">Tel</Label>
<Input
type="tel"
name="tel"
id="tel"
value={values.tel}
onChange={handleChange}
onBlur={handleBlur}
invalid={touched.tel && errors.tel ? true : false}
/>
<FormFeedback>
{errors.tel}
</FormFeedback>
</FormGroup>
<div style={{ textAlign: 'center' }}>
<Button color="success" type="submit" disabled={this.state.loading}>
<Spinner size="sm" color="light" style={{ marginRight: 5 }} hidden={!this.state.loading} />
新規登録
</Button>
</div>
</Form>
)
}
</Formik>
</div>
<div className="mx-auto" style={{ width: 400, background: '#fff', padding: 20 }}>
<Link to="/signin">ログインはこちら。</Link>
</div>
</div>
);
}
}
export default withRouter(SignUp);
SignInOrUp.js
つぎにSignInOrUp(ログイン)画面とログイン機能を実装します。
import React from 'react';
import { Button, Form, FormGroup, Label, Input, FormFeedback, Spinner } from 'reactstrap';
import { Link, withRouter } from 'react-router-dom'
import { Formik } from 'formik';
import * as Yup from 'yup';
import firebase from '../Firebase';
class SignInOrUp extends React.Component {
state = {
loading: false, //spinner制御用
}
_isMounted = false;
handleOnSubmit = (values) => {
//spinner表示開始
if (this._isMounted) this.setState({ loading: true })
//サインイン(ログイン)処理
firebase.auth().signInWithEmailAndPassword(values.email, values.password)
.then(res => {
//正常終了時
this.props.history.push("/");
if (this._isMounted) this.setState({ loading: false });
})
.catch(error => {
//異常終了時
if (this._isMounted) this.setState({ loading: false });
alert(error);
});
}
componentDidMount = () => {
this._isMounted = true;
}
componentWillUnmount = () => {
this._isMounted = false;
}
render() {
return (
<div className="container">
<div className="mx-auto" style={{ width: 400, background: '#eee', padding: 20, marginTop: 60 }}>
<p style={{ textAlign: 'center' }}>サインイン</p>
<Formik
initialValues={{ email: '', password: '' }}
onSubmit={(values) => this.handleOnSubmit(values)}
validationSchema={Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().required(),
})}
>
{
({ handleSubmit, handleChange, handleBlur, values, errors, touched }) => (
<Form onSubmit={handleSubmit}>
<FormGroup>
<Label for="email">Email</Label>
<Input
type="email"
name="email"
id="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
invalid={touched.email && errors.email ? true : false}
/>
<FormFeedback>
{errors.email}
</FormFeedback>
</FormGroup>
<FormGroup>
<Label for="password">Password</Label>
<Input
type="password"
name="password"
id="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
invalid={touched.password && errors.password ? true : false}
/>
<FormFeedback>
{errors.password}
</FormFeedback>
</FormGroup>
<div style={{ textAlign: 'center' }}>
<Button color="primary" type="submit" disabled={this.state.loading}>
<Spinner size="sm" color="light" style={{ marginRight: 5 }} hidden={!this.state.loading} />
ログイン
</Button>
</div>
</Form>
)
}
</Formik>
</div>
<div className="mx-auto" style={{ width: 400, background: '#fff', padding: 20 }}>
<Link to="/signup">新規登録はこちら。</Link>
</div>
</div>
);
}
}
export default withRouter(SignInOrUp);
Home.js
Home画面は何でもいいのですが、せっかくなのでサインアウト(ログアウト)機能を実装してみます。
import React from 'react';
import firebase from '../Firebase';
import { Link } from 'react-router-dom';
import { Button } from 'reactstrap';
class Home extends React.Component {
handleLogout = () => {
firebase.auth().signOut();
}
render() {
return (
<div className="container">
<p>Home</p>
<Link to="/profile">Profileへ</Link>
<br />
<br />
<Button onClick={this.handleLogout}>ログアウト</Button>
</div>
);
}
}
export default Home;
Profile.js
ただ表示するだけ。いちおうHome画面に戻るボタンを付けてみます。
import React from 'react';
import {Link} from 'react-router-dom';
class Profile extends React.Component {
render() {
return (
<div className="container">
<p>Profile</p>
<br/>
<Link to="/">Homeへ</Link>
</div>
);
}
}
export default Profile;
動作確認
実装が完了したら動作確認してみます。
yarn start