ReactでFirebase Authenticationを使う


## やりたいこと

下記のような感じ。
ログイン必要ページと不要ページを作成し、ログイン必要ページへのアクセスにはチェックを入れる。

かつ、それをreact-router-domで、わかりやすくきれいに書きたい。

![スクリーンショット 2019-11-23 7.34.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/c2419214-b4ee-e08c-6aa9-223295453b53.png "スクリーンショット 2019-11-23 7.34.35.png")

## 完成イメージ

各ファイルの画面イメージ（画面のあるもの）は下記ような感じ。

![スクリーンショット 2019-11-23 8.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/2c1e7c15-5261-4168-7e2d-318204279a71.png "スクリーンショット 2019-11-23 8.18.30.png")

## 準備

### 雛形の作成とモジュールのインストール

create-react-appを利用します。
作業フォルダ作成後、必要なモジュールをインストールします。

```bash
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

### 必要なファイルの生成

必要なファイルを生成しておきます。階層に気をつけながら生成します。

```bash
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で読み込んでおきます。

```diff: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を設定します。

```js: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>で囲ったところが認証エリアになるようにします。

```js:App.js
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）に時間がかかるため、チェック処理が完了済みかどうかをチェックし、チェックが未完了ならローディング画面を表示し、チェック完了を待ちます。

完了すれば、状況に応じて処理を進めます。

```js:Auth.js
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から実装してみます。
バリデーション等で少々複雑に見えますが大したことはしていません。

```js:SignUp.js
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（ログイン）画面とログイン機能を実装します。

```js:SignInOrUp.js
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画面は何でもいいのですが、せっかくなのでサインアウト（ログアウト）機能を実装してみます。

```js:Home.js
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画面に戻るボタンを付けてみます。

```js:Profile.js
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;
```

### 動作確認

実装が完了したら動作確認してみます。


```bash
yarn start
```