ReactとFirebaseを使ってログインフォームを実装する②

今回はログインフォームを作成していきます。reduxやredux-thunk、Firebaseを使うことになります。 ログイン状態をreduxで管理し、まずフロント側の処理を完成させることを目指します。

# Firebaseの下準備
ログインフォームを作成する前にFirebaseの使う準備をします。
## Firebaseのプロジェクトを作る

https://console.firebase.google.com/にアクセスしてFIrebaeプロジェクトを作りましょう。Googleアカウントが必要です。
Google アナリティクスのチェックは今回は使わないので外しておきましょう。少し時間がかかってプロジェクトが作成されます。

ウェブアプリにFirebase を追加、下記のアイコンをクリックします。
![react-firebase-login2-1-800x467.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401408/24cd8e05-c334-4dad-7f87-bad6b1202197.png)
アプリのニックネームを入力してアプリを登録をクリックします。Firebaes Hostingの設定はチェックしなくて大丈夫です。
![react-firebase-login2-2-800x467.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401408/7587df84-55b6-06c7-cee8-4cce49e12cc5.png)
コードが表示されるのでvar firebaseConfig = { ~ }を記録しといてください。
![react-firebase-login2-3-800x482.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401408/3b427843-50a2-ba84-e40a-96c2fcf287ec.png)

## プロジェクトでfirebaseを使う下準備
ローカルにもどります。firebaseパッケージをインスト―ルしましょう。

```bash
yarn add firebase
```
さっき記録した firebaseConfig = { ~ } を使うのですが、直接参照するのではなく環境変数から参照するようにします。ルートディレクトリに.envファイルを作成します。.envに記録したapiキーを書きましょう。

環境変数を参照するにはdotenvモジュールが必要ですが、 create-react-app自体にすでにdotenvが組み込まれています。

```bash:.env
REACT_APP_FB_API_KEY=XXXXXXXXX
REACT_APP_FB_AUTH_DOMAIN=XXXXXXXXX
REACT_APP_FB_DATABASE_URL=XXXXXXXXX
REACT_APP_FB_PROJECT_ID=XXXXXXXXX
REACT_APP_FB_STORAGE_BUCKET=XXXXXXXXX
REACT_APP_FB_MESSAGING_SENDER_ID=XXXXXXXXX
REACT_APP_FB_APP_ID=XXXXXXXXX
```

firebaseの設定を読み込みます。src/configディレクトリ以下にfirebase.jsを作成します。

```javascript:src/config/firebase.js
import firebase from 'firebase/app';
import 'firebase/auth';

const config = {
  apiKey: process.env.REACT_APP_FB_API_KEY,
  authDomain: process.env.REACT_APP_FB_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_FB_DATABASE_URL,
  projectId: process.env.REACT_APP_FB_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FB_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FB_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FB_APP_ID,
};

firebase.initializeApp(config);

export default firebase;
```

これでfirebaseを使う下準備は完了です！このfirebase.jsからモジュールをインポートして使用します。

# ログインフォームを作成
ログインフォームの画面を完成させます。
## Reduxを使用する準備
reduxを使うので準備します。firebaseとの通信にredux-thunkを使うのでミドルウェアの設定もします。ついでにデバックに便利なRedux DevToolsの設定もしておきましょう。

Redux DevToolsについては下記参照
https://harkerhack.com/react-redux-devtools/
index.jsを編集しましょう。redux、redux-thunk、 Redux DevToolsの設定をしています。

```javascript:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import reduxThunk from 'redux-thunk';

import App from './App';
import reducers from './reducers';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk)));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector('#root')
);
```

reducersとactionsディレクトリを作成します。src以下に配置。
ディレクトリに対してインポートできるようにreducersとactionsディレクトリ以下にindex.jsを配置します。認証用のauth.jsとAuthReducer.jsをそれぞれ作成します。

actionとreducerのtypeでの文字列によるエラー防止のため 、actionsディレクトリ内にtype.jsを作成し、そこから変数を読み込むようにします。 複雑なので画像を参照してください。
![react-firebase-login2-4-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401408/d3e2b057-3732-a9d2-e037-d81c51bcacce.png)

## ログインフォームのコンポーネントを作成
ダイアログで表示するためのコンポーネントを作成します。ソーシャルログインのためのボタンを用意しましょう。react-social-login-buttonsを使用します。

react-social-login-buttons参考URL
https://www.npmjs.com/package/react-social-login-buttons

```bash
yarn add react-social-login-buttons
```
components内にLoginForm.jsxを作成します。 LoginFormをヘッダーでダイアログ表示します。長いのでコメントで解説しています。

親のコンポーネントからthis.props.formTextとして文字列を受け取って、ログインとサインアップ処理を切り替えています。firebaseとの通信はaction内で行います。

```javascript:src/components/LoginForm.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import { emailChanged, passwordChanged, loginUser, signUpUser, loggined } from '../actions';
import { FacebookLoginButton, GoogleLoginButton, TwitterLoginButton } from 'react-social-login-buttons';
import CircularProgress from '@material-ui/core/CircularProgress';
import firebase from '../config/firebase';

class LoginForm extends Component {
  onEmailandPasswordLogin = () => {
    // actionでログイン処理
    const { email, password } = this.props;
    this.props.loginUser({ email, password });
  };

  onEmailandPasswordSignUp = () => {
    // actionでサインアップ処理
    const { email, password } = this.props;
    this.props.signUpUser({ email, password });
  };

  onButtonPress = () => {
    // formtextでログインとサインアップを切り替え
    if (this.props.formText === 'ログイン') {
      this.onEmailandPasswordLogin();
    } else {
      this.onEmailandPasswordSignUp();
    }
  };

  onEmailChange = email => {
    // フォームにテキストが入力されたらreduxのstoreを更新
    this.props.emailChanged(email.target.value);
  };

  onPasswordChane = password => {
    // フォームにテキストが入力されたらreduxのstoreを更新
    this.props.passwordChanged(password.target.value);
  };

  googleLogin = () => {
    // Googleログイン処理
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithRedirect(provider);
  };

  facebookLogin = () => {
    // フェイスブックログイン処理
    const provider = new firebase.auth.FacebookAuthProvider();
    firebase.auth().signInWithRedirect(provider);
  };

  twitterLogin = () => {
    // ツイッターログイン処理
    const provider = new firebase.auth.TwitterAuthProvider();
    firebase.auth().signInWithRedirect(provider);
  };

  renderErrorMessage = () => {
    // エラーメッセージ
    const { error } = this.props;
    if (error === 'auth/user-not-found') {
      return 'ユーザー情報が登録されていません。';
    } else if (error === 'auth/wrong-password') {
      return 'パスワードが間違っています。';
    } else if (error === 'auth/invalid-email') {
      return 'メールアドレスが正しくありません。';
    } else if (error === 'auth/weak-password') {
      return 'パスワードは少なくとも6文字以上必要です。';
    } else if (error === 'auth/email-already-in-use') {
      return 'このメールアドレスは既に登録されています。';
    }
  };

  render() {
    return (
      <div>
        <GoogleLoginButton onClick={this.googleLogin} align="center" iconSize={'20'} size={'40'}>
          <span style={{ fontSize: 16 }}>Googleで{this.props.formText}</span>
        </GoogleLoginButton>
        <TwitterLoginButton onClick={this.twitterLogin} align="center" iconSize={'20'} size={'40'}>
          <span style={{ fontSize: 16 }}>Twitterで{this.props.formText}</span>
        </TwitterLoginButton>
        <FacebookLoginButton onClick={this.facebookLogin} align="center" iconSize={'20'} size={'40'}>
          <span style={{ fontSize: 16 }}>Facebookで{this.props.formText}</span>
        </FacebookLoginButton>
        <div style={{ textAlign: 'center', marginTop: 20 }}>または</div>
        <form style={{ textAlign: 'center' }} noValidate autoComplete="off">
          <div>
            <TextField
              id="standard-email"
              label="メールアドレス"
              value={this.props.email}
              onChange={this.onEmailChange}
              margin="normal"
            />
          </div>
          <div>
            <TextField
              id="standard-password"
              label="パスワード"
              type="password"
              value={this.props.password}
              onChange={this.onPasswordChane}
              margin="normal"
            />
          </div>
          <div style={{ color: '#fa755a' }}>{this.renderErrorMessage()}</div>
          {this.props.loading ? (
            <CircularProgress style={{ marginTop: 5 }} />
          ) : (
            <Button style={{ margin: 20 }} onClick={this.onButtonPress} variant="contained" color="primary">
              {this.props.formText}
            </Button>
          )}
        </form>
      </div>
    );
  }
}

// Reduxと繋げる
const mapStateToProps = ({ auth }) => {
  return {
    email: auth.email,
    password: auth.password,
    error: auth.error,
    loading: auth.loading,
  };
};

export default connect(mapStateToProps, {
  emailChanged,
  passwordChanged,
  loginUser,
  loggined,
  signUpUser,
})(LoginForm);
```

## actionsを書く
actionを書きましょう。ログイン処理でのfirebaseとの通信にredux-thunkで非同期処理をしています。 それぞれのactionは以下です。

- Eメールのテキストフォームの変更
- パスワードのテキストフォームの変更
- Eメールとパスワードでのログイン処理
- Eメールとパスワードでのサインアップ処理
- ログイン状態での処理
- ログアウト状態の処理

ログインとサインアップはfirebaseの処理が違うだけでほとんど同じです。詳しくはコメント参照

```javascript:src/actions/auth.js
import firebase from '../config/firebase';

import {
  EMAIL_CHANGED,
  PASSWORD_CHANGED,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAIL,
  LOGIN_USER,
  SIGN_UP_USER,
} from './types';

////////////////////emailとpasswordでサインアップ、ログイン用/////////////////////////////
export const emailChanged = text => {
  // Eメールのテキストフォームの変更
  return {
    type: EMAIL_CHANGED,
    payload: text,
  };
};

export const passwordChanged = text => {
  // パスワードのテキストフォームの変更
  return {
    type: PASSWORD_CHANGED,
    payload: text,
  };
};

export const loginUser = ({ email, password }) => async dispatch => {
  // emailとpasswordでログインする
  // ログイン開始
  dispatch({
    type: LOGIN_USER,
  });

  firebase
    .auth()
    .signInWithEmailAndPassword(email, password)
    .then(user => {
      // ログイン成功、ユーザー情報を送る
      dispatch({
        type: LOGIN_USER_SUCCESS,
        payload: user,
      });
    })
    .catch(error => {
      console.log(error);
      // ログイン失敗、エラーコードを送る
      dispatch({
        type: LOGIN_USER_FAIL,
        payload: error.code,
      });
    });
};

export const signUpUser = ({ email, password }) => async dispatch => {
  // emailとpasswordでサインアップする
  // サインアップ開始
  dispatch({
    type: SIGN_UP_USER,
  });

  firebase
    .auth()
    .createUserWithEmailAndPassword(email, password)
    .then(user => {
      // サインアップ成功、ユーザー情報を送る
      dispatch({
        type: LOGIN_USER_SUCCESS,
        payload: user,
      });
    })
    .catch(error => {
      console.log(error);
      // サインアップ失敗、エラーコードを送る
      dispatch({
        type: LOGIN_USER_FAIL,
        payload: error.code,
      });
    });
};

////////////////////ログイン処理/////////////////////////////
// すでにログインしていた時の処理
export const loggined = () => async dispatch => {
  const user = firebase.auth().currentUser;
  dispatch({
    type: LOGIN_USER_SUCCESS,
    payload: user,
  });
};

export const logouted = () => {
  return {
    type: LOGIN_USER_FAIL,
    payload: null,
  };
};
```
type.jsを書きます。これはreducerとactionでエラーが起きないように（エラーの場所がわかりやすいように）するためです。

```javascript:src/actions/type.js
export const EMAIL_CHANGED = 'email_changed';
export const PASSWORD_CHANGED = 'password_changed';
export const LOGIN_USER_SUCCESS = 'login_user_success';
export const LOGIN_USER_FAIL = 'login_user_fail';
export const LOGIN_USER = 'login_user';
export const SIGN_UP_USER = 'sign_up_user';
```

actions内のindex.jsでauth.jsを読み込みエクスポートしましょう。これでactionsディレクトリごとインポートできるようになります。

```javascript:src/actions/index.js
export * from './auth';
```

## reducerを書く
actionsから送られてきた情報を処理しましょう。AuthReducer.jsを書きます。コメントを参照してください。ステートはそれぞれ以下です。

- Eメール
- パスワード
- ユーザー情報
- ロード中か
- ログインしているか

```src/reducers/AuthReducer.js
import {
  EMAIL_CHANGED,
  PASSWORD_CHANGED,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAIL,
  LOGIN_USER,
  SIGN_UP_USER,
} from '../actions/types';

const INITIAL_STATE = {
  email: '',
  password: '',
  user: null,
  error: '',
  // ロード中か
  loading: false,
  // ログインしているか
  isLoggedIn: null,
};

export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case EMAIL_CHANGED:
      // Eメール変更
      return {
        ...state,
        email: action.payload,
      };
    case PASSWORD_CHANGED:
      // パスワード変更
      return {
        ...state,
        password: action.payload,
      };
    case LOGIN_USER:
      // ログイン処理開始、ロード中にする
      return {
        ...state,
        loading: true,
        error: '',
      };
    case SIGN_UP_USER:
      // サインアップ処理開始、ロード中にする
      return {
        ...state,
        loading: true,
        error: '',
      };
    case LOGIN_USER_SUCCESS:
      // ログイン成功、ロード修了、ユーザー情報取得、ログイン状態にする
      return {
        ...state,
        loading: false,
        password: '',
        user: action.payload,
        isLoggedIn: true,
      };
    case LOGIN_USER_FAIL:
      // ログイン失敗、ロード修了、ログアウト状態にする
      return {
        ...state,
        error: action.payload,
        loading: false,
        password: '',
        isLoggedIn: false,
      };
    default:
      return state;
  }
};
```

reducer内のindex.jsで読み込みます。combineReducersで今後Reducerが増えても処理できるようにしています。

```javascript:src/reducers/index.js
import { combineReducers } from 'redux';
import AuthReducer from './AuthReducer';

export default combineReducers({
  auth: AuthReducer,
});
```

# ログインしている時だけにLoginedPageに移動するようにする
ログアウト状態のときに、ログイン後のページにアクセスできたら困るので、ログインしているときだけLoginedPageにアクセスできるようにしましょう。
そのためにApp.jsxと同じディレクトリにAuth.jsxコンポーネントを作成します。コメント参照。

```jsx:src/Auth.jsx
import React, { Component, Fragment } from 'react';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';

class Auth extends Component {
  renderContent = () => {
    if (this.props.isLoggedIn) {
      // ログインしていたら子コンポーネントを表示する
      return this.props.children;
    } else {
      // ログインしてないならリダイレクト
      return <Redirect to={'/'} />;
    }
  };
  render() {
    return <Fragment>{this.renderContent()}</Fragment>;
  }
}

const mapStateToProps = state => {
  return { isLoggedIn: state.auth.isLoggedIn };
};

export default connect(mapStateToProps)(Auth);
```

App.jsxのcomponentDidMount内でfIrebaseからログイン情報を取得して、storeにログイン情報を保存します。その情報をもとにルーティングを決定します。

firebaseからログイン情報を受け取る方法は下記参照
https://qiita.com/niusounds/items/829780bdc45d34b4d1e7

App.jsxでAuthコンポーネントをインポートします。 Authコンポーネントで囲むことでログイン状態のときだけLoginedPageにアクセスできるようになります。

```javascript:src/App.jsx
import React, { Component } from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import firebase from './config/firebase';

import { loggined, logouted } from './actions';
import history from './history';
import LandingPage from './components/LandingPage';
import LoginedPage from './components/LoginedPage';
import NavBar from './components/NavBar';
import Auth from './Auth';

class App extends Component {
  componentDidMount() {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        // ログイン処理
        this.props.loggined();
        console.log('loginしました');
      } else {
        // ログアウト処理
        this.props.logouted();
      }
    });
  }
  render() {
    return (
      <Router history={history}>
        <NavBar />
        <Switch>
          <Route path="/" exact component={LandingPage} />
          <Auth>
            <Route path="/logined" exact component={LoginedPage} />
          </Auth>
        </Switch>
      </Router>
    );
  }
}

const mapStateToProps = state => {
  return { isLoggedIn: state.auth.isLoggedIn };
};

export default connect(mapStateToProps, { loggined, logouted })(App);
```

# LoginFormコンポーネントを表示してみる
最終的にはダイアログ表示にしますが、ここで一旦ログインフォームを表示してみましょう。LandingPage.jsxを編集します。 formText={'ログイン'} でログイン処理になります。

```jsx:src/LandingPage.jsx
import React, { Component } from 'react';
import LoginForm from './LoginForm';

class LandingPage extends Component {
  render() {
    return (
      <div>
        LandingPage
        <LoginForm formText={'ログイン'} />
      </div>
    );
  }
}

export default LandingPage;
```

yarn startでローカルサーバー起動します。 http://localhost:3000/ にアクセス。
横幅いっぱいに広がっていますがそれっぽいのが表示されています！

![react-firebase-login2-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401408/5895521e-bb9a-bb1b-8f34-edff18fb4b80.png)

# おわり
お疲れさまでした！今回は一番の山場でした。reduxを使用したステート管理とredux-thunkを使用した非同期処理。ログイン状態によるリダイレクト処理など盛りだくさんでした。

ログインフォームを作成するフロント側の処理はほとんど完了です。後はfirebaseでの処理とログインフォームのダイアログ表示、ログアウト処理になります。

# 全4回
- [ReactとFirebaseを使ってログインフォームを実装する①](https://qiita.com/harker/items/1504264134dd2278cf2e)
- [ReactとFirebaseを使ってログインフォームを実装する②](https://qiita.com/harker/items/a3f4e2b2663664a206e1)
- [ReactとFirebaseを使ってログインフォームを実装する③](https://qiita.com/harker/items/678f72b91ffa73f23cc9)
- [ReactとFirebaseを使ってログインフォームを実装する④](https://qiita.com/harker/items/6cf4f3f2ee0ff7571660)