React＋Redux+Firebaseでログイン状態を管理する

## はじめに
Firebaseによるログイン機能を実装されている[チャットアプリ](https://react-chat-b0e8a.firebaseapp.com/)に、**Redux**を追加しました。
`state`の遷移をわかりやすくすること、コードの分割が目的です。
この記事はその際の記録、説明となります。

## GithubのPullRequest
自分が行った試行錯誤の記録、実際のコード変更などはこちらから確認してください。

**[feat: Setting Redux](https://github.com/kaibara/React-chat/pull/24)**

## 現在の状態

- URL: [Reactチャット](https://react-chat-b0e8a.firebaseapp.com/)
- Github: [kaibara/React-chat](https://github.com/kaibara/React-chat)

## Reduxとは
まずは**Redux**について説明していきます。
以下の記事を参考にさせていただきました。

- Fluxの説明:
[react+reduxで非同期処理を含むtodoアプリを作ってみる](https://qiita.com/halhide/items/a45c7a1d5f949596e17d#redux)

- Reduxの説明:
[Reduxの実装とReactとの連携を超シンプルなサンプルを使って解説](https://mae.chab.in/archives/2885#post2885-1)

### Flux
**Redux**を理解するためには**Flux**を理解する必要があります。
Reactで作成するファイル、使用するコードで管理しているのは`View`、フロント部分です。
データなどを管理することも可能ですが、親componentから子componentへstateの受け渡しならまだしも、親から孫componentまでデータの受け渡しをするとなれば、コードが複雑になっていきます。

それを解消、抑制するのが**Flux**という考え方です。
これはFacebookが提唱しています。
**Flux**では、データの流れを一方向で管理するように言われています。
具体的な流れは以下のようになります。

```
Action -> Dispatcher -> Store -> View(ページ)
```

それぞれの役割を簡単に説明すると、以下のようになります。

```
Action: ボタンをクリックするなどstateが変更するイベントなどを定義
Dispatch: Actionを受けて変更するstateをStoreに伝える
Store: Dispatchの指示を受けて特定のstateを再定義する
View: stateが変更されユーザーが見るページが変化する
```

このようなデータの流れにすることで、それぞれの役割を簡潔にできます。
それだけでなく、コードも分割されるためデータの流れもわかりやすくできます。
後で機能を追加するときも作業が楽になるでしょう。

### Redux
**Redux**は**Flux**の考えを模していますが、**Fluxそのものではありません。**
**Fluxは考え方の一つです。**
**Reduxはそれに近い機能を実装したフレームワーク**に過ぎません。
**FluxとReduxを同一視しないようにしてください。**

**Redux**は**Flux**の考え方を模しているため、データの流れは**Flux**で説明したものとほとんど同じです。
ただ、**Redux**には**Dispatcher**がありません。
その代わりとして**Reducer**を用いるのです。

**Reducer**では、`state`と受け取った**Action**を引数に新しい`state`を返す関数が定義されます。
`switch`で`type`を判別して処理を実装するのです。
**Reducer**は複数作成できることも特徴の1つと言えるでしょう。

必要最低限の情報だけをご説明しました。
わからないことがあれば、ぜひ調べてみてください。
では、**Redux**を実装していきます。

## 実践
自分は次の記事を参考に、コードを作成していきました。

- [React+Redux+Firebase Authenticationでログイン/ログアウト機能を実装する (1)](https://qiita.com/k5trismegistus/items/2b82c9721e5d245b54ca)
- [React+Redux+Firebase Authenticationでログイン/ログアウト機能を実装する (2)](https://qiita.com/k5trismegistus/items/7aa3b61d3815a4474016)

### ファイル構成
このアプリは**Redux**を実装する前、以下のファイル構成となっています。

#### Before

```
src ー index.js
　　 |ー components
　　 |     　|ー ChatMessage.js
　　 |     　|ー ChatForm.js
　　 |     　|ー App.js
 　　|
　　 |ー firebase
　　　　　　　|ー firebase.js
```

##### Before Codes


<details>
<summary>src/index.js</summary>
<div>

```jsx:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
```

</div>
</details>

<details>
<summary>src/components/ChatMessage.js</summary>
<div>

```jsx:src/components/ChatMessage.js
import React,{Component} from 'react'
import { firebaseDB } from '../firebase/firebase'

const messagesRef = firebaseDB.ref('messages')

class ChatMessage  extends Component {
  constructor(props) {
    super(props)
    this.onRemoveClick = this.onRemoveClick.bind(this)
  }
  onRemoveClick(){
     messagesRef.child(this.props.message.key).remove()
     alert('メッセージはページを更新した後に削除されます')
  }
  render(){
    return(
      <div>
        <p>{this.props.message.text}</p>
        <p>by&nbsp;{this.props.message.user_name}</p>
        <button onClick={this.onRemoveClick}>削除</button>
      </div>
    )
  }
}

export default ChatMessage
```

</div>
</details>

<details>
<summary>src/componets/ChatForm.js</summary>
<div>

```jsx:src/components/ChatForm.js
import React,{Component} from 'react'
import firebase from 'firebase/app'
import {firebaseApp} from '../firebase/firebase'

class ChatForm extends Component {
  constructor(props){
    super(props)
    this.state = {
      user: null
    }
  }
  componentDidMount(){
    firebase.auth().onAuthStateChanged(user => {
      this.setState({ user })
    })
  }
  login() {
    const provider = new firebase.auth.GoogleAuthProvider()
    firebase.auth().signInWithPopup(provider)
  }
  logout(){
    firebase.auth().signOut()
  }
  change(){
    this.setState({user_name: this.state.user.displayName})
  }
  render(){
    return(
      <div>
        {this.state.user ?
          <div>
            <h2>{this.state.user.displayName}</h2>
              <div>
                <button name='user_name' value={this.state.user.displayName} onClick={this.props.onTextChange}>この名前を使う</button>
                <button onClick={this.logout}>違う名前を使う</button>
              </div>
          </div> :
          <div>
            <input name='user_name' onChange={this.props.onTextChange} placeholder='名前' style={inputStyle}/>
            <button onClick={this.login}>Goggle Login</button>
          </div>
        }
        <textarea name='text' onChange={this.props.onTextChange}  placeholder='メッセージ'/>
        <button className='send' onClick={this.props.onButtonClick}>送信</button>
      </div>
    )
  }
}

export default ChatForm

```

</div>
</details>

<details>
<summary>src/components/App.js</summary>
<div>

```jsx:src/components/App.js
import React, { Component } from 'react'
import firebase from 'firebase/app'
import { firebaseApp,firebaseDB } from './firebase/firebase'
import ChatMessage from './components/ChatMessage'
import ChatForm from './components/ChatForm'

const messagesRef = firebaseDB.ref('messages')

class App extends Component {
  constructor(props) {
    super(props)
    this.onTextChange = this.onTextChange.bind(this)
    this.onButtonClick = this.onButtonClick.bind(this)
    this.state = {
      text : "",
      user_name: "",
      messages : []
    }
  }

  componentWillMount() {
      messagesRef.on('child_added', (snapshot) => {
        const m = snapshot.val()
        let msgs = this.state.messages
        console.log({msgs})
        msgs.push({
          'text' : m.text,
          'user_name' : m.user_name,
          'key': snapshot.key
        })
        this.setState({ messages : msgs })
        console.log(this.state.messages)
      })
    }

  onTextChange(e) {
    if(e.target.name == 'user_name') {
      this.setState({
        "user_name": e.target.value
      });
    } else if (e.target.name == 'text') {
      this.setState({
        "text": e.target.value
      });
    }
  }
  onButtonClick() {
    if(this.state.user_name == "") {
      alert('名前を入力してください')
      return
    } else if(this.state.text == "") {
      alert('メッセージを入力してください')
      return
    }
    messagesRef.push({
      "user_name" : this.state.user_name,
      "text" : this.state.text
    })
    this.setState({"text": ""})
  }

  render() {
    return (
      <div>
        <div>
          <h2>メッセージログ</h2>
          {this.state.messages.map((m, i) => {
            return <ChatMessage key={i} message={m} />
          })}
        </div>
        <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    )
  }
}

export default App;
```

</div>
</details>

<details>
<summary>src/firebase/firebase.js</summary>
<div>

```jsx:src/firebase/firebase.js
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/database'

const config = {
  apiKey: "***",
  authDomain: "***",
  databaseURL: "***",
  projectId: "***",
  storageBucket: "***",
  messagingSenderId: "***"
}

export const firebaseApp = firebase.initializeApp(config)
export const firebaseDB = firebase.database()

```

</div>
</details>

#### After

変更後は以下のようになります。

```
src ー index.js
　　 |ー components
　　 |     　|ー ChatMessage.js
　　 |     　|ー ChatForm.js
　　 |     　|ー App.js
 　　|
　　 |ー firebase
　　 |　　　　|ー firebase.js
　　 |
　　 |ー actions
　　 |　　　　|ー Auth.js
　　 |
　　 |ー reducers
　　 |　　　　|ー index.js
　　 |　　　　|ー auth.js
　　 |
　　 |ー containers
　　  　　　　|ー AppContainer.js
　　  　　　　|ー ChatFormContainer.js
```


### Firebase
この記事ではFirebaseによるログイン機能の実装方法について触れません。
Firebaseによるログイン機能ができていない人は、次の記事を参考に実装してみてください。

- [ReactにFirebaseを使ったログイン機能を実装する](https://qiita.com/Takumi_Kaibara/items/6cfbcfed55cc6e817b20)

### Reduxの導入
[チャットアプリ](https://react-chat-b0e8a.firebaseapp.com/)は`npm`で管理しています。
`npm`の環境では次のコマンドで**Redux**をインストールできます。

```sh
 $ npm install --save redux react-redux
```

**Redux**だけであれば、`redux`だけをインストールすれば良いですが、このアプリではReactでコードを作成しています。
ReactとReduxを連携させるためには`react-redux`が必要になるため、欠かさずインストールするようにしてください。


### AppContainerの実装
ReactとReduxを使用するアプリでは**container**と呼ばれるものを使用します。
それは**Reduxが管理するstateとReactで定義するviewをまとめたcomponent**です。
まとめる際には`react-redux`の`connect`を使用します。
**container**によってまとめられたcomponentを他の場所呼び出して、アプリを設計していくのです。

まずはこのアプリにおけるrootコンポーネントである`src/components/App.js`にで**Redux**で操作する`state`を定義し、扱えるようにしていきます。

#### ReduxState
このアプリで**Redux**に管理してもらう`state`を定義します。
ここで定義したstateは`reducers/auth.js`で実装します。
他のstateを定義する際は`reducers/~.js`と新しいファイルを作成して設定してください。

```ReduxState
auth: {
  displayName: ''
}
```

#### src/reducers/index.js & src/reducers/auth.js
では、現在の`state`とそれに応じた**Action**の内容に合わせて`state`を変更する**reducer**を設定していきます。
ここでは以下の記事を参考にしました。

- [Redux入門 3日目 Reduxの基本・Reducers(公式ドキュメント和訳)](https://qiita.com/kiita312/items/7fdce94912d6d9c801f8) 

`reducers`ディレクトリを作成し、全ての**Reducer**を１つにまとめる`index.js`と今回`Redux`で扱う`state.auth`を設定する`auth.js`を作成します。

```sh
 $ mkdir src/reducers
 $ touch src/reducers/index.js src/reducers/auth.js
```

`auth.js`から解説していきます。

```jsx:src/reducers/auth.js
const initialState = {
  displayName: null
}
//ReduxStateで定義したauthの設定

const auth = (state=initialState, action) => {
  switch (action.type) {
    default: {
      return state
    }
  }
}
//今後作成するactionの内容に応じてswitchにcaseを追加します。
//今は何もactionを指定していないため、dafaultだけ作成してください。

export default auth
```

続いて、`index.js`です。

```jsx:src/reducers/index.js
import { combineReducers } from 'redux'
import auth from './auth'

const Reducer = combineReducers({auth})
//このアプリで使用するreducerをひとまとめに。
//追加する場合は、({auth,~,~})という風に

export default Reducer
```

後から`auth.js`に**Action**を追加していくことを覚えておいて次の作業に移りましょう。

#### src/containers/AppContainer.js
次に`src/components/App.js`と**Redux**の`state`を結びつける**container**を作成します。

```jsx/src/containers/AppContainer.js
import firebase from 'firebase/app'
import { connect } from 'react-redux'
import App from '../components/App'

const mapStateToProps = (state) => {
  console.log(state.auth.displayName)
  return {
    displayName: state.auth.displayName,
    //reducers/auth.jsの中にあるdisplayNameを取り出す
  }
}
//App.jsで使用するReduxのstateを設定

const AppContainer = connect(
  mapStateToProps
)(App)
//src/App.jsとReduxが管理するstateの結びつけ

export default AppContainer
```

やっていることは、**Reduxの中からReactで扱う`state`を呼び出して、`connect`で結びつける**だけです。

#### src/index.js
最後に、`src/index.js`にアプリ全体で**Redux**を使用できるようにします。

```jsx:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import AppContainer from './containers/AppContainer';
import {createStore} from 'redux';
import Reducer from './reducers/index.js'
import { Provider } from 'react-redux';

const store = createStore(Reducer)
//Storeの作成

ReactDOM.render(
  <Provider store={store}>
　//stateを管理するStoreをアプリ全体に適用する
    <AppContainer />
  </Provider>, document.getElementById('root'));
```

ここではアプリ全体で使用している`state`を管理する**Store**を作成しました。
そして、`Provider`を使用し、`state`をアプリ全体で使用できるようにしたのです。
**Store**の詳しい内容などは下の記事を参考にしてみてください。

- [Redux入門 4日目 Reduxの基本・Stores(公式ドキュメント和訳)](https://qiita.com/kiita312/items/377787c24efac64f2495)

#### 確認する
ここまでできたら、**Redux**が正常に稼働しているかを確認してください。
`src/containers/AppContainer.js`の`console.log(state.auth.displayName)`を見ます。
chromeなら開発者ツールのconsoleで`null`が表示されていれば成功です。
<img src="https://qiita-image-store.s3.amazonaws.com/0/218398/8e1de391-29ce-8464-21b1-3c941903e4f9.png" width="600px">

#### Git Commit
[feat: Create AppContainer](https://github.com/kaibara/React-chat/commit/be7a4dc778053ff336a9c0b227808a485514c880)

### ChatFormContainerの作成
`AppContainer.js`によってアプリで**Redux**を使用する準備ができました。
そしたら**Redux**の`state`を使用、操作するContainerを作成していきましょう。

#### src/actions/Auth.js
最初に**Action**の定義をします。
今回は**Redux**でユーザー名を管理するため、以下の設定をします。

- **ログイン時はFirebaseの`user.displayName`**
- **ログアウト時は何もしない**

以上を基にファイルを作成します。

```jsx:src/actions/Auth.js
export const LOGIN = 'LOGIN'
export const LOGOUT = 'LOGOUT'
//他のファイル(src/reducers/auth.js)で使用するためここで定義

export const loginAction = (user) => {
  return {
    type: LOGIN,
    payload: {
      displayName: user.displayName
    }
  }
}
//実行される時、this.state.auth.displayNameにFirebaseのユーザー名が入るuser.displayNameを入力する

export const logoutAction = () => {
  return {
    type: LOGOUT
  }
}
//実行される時、typeだけを記録する
```

**Action**の細かい内容については下記の記事を参考にしてみてください。

- [Redux入門 2日目 Reduxの基本・Actions(公式ドキュメント和訳)](https://qiita.com/kiita312/items/8f8d047e5cbd87399ccb)

#### src/reducers/auth.js
作成した**Actions**のコードが実行された際、命令が**Reducer**に送られます。
**Reducer**の中から送られてきた**Actions**の`type`と一致したら、その命令が実行されるのです。

`AppContainer.js`の作成時にも`src/reducers/auth.js`を操作しました。
しかし、まだ**Action**から送られてきた`type`を識別するためのコードがないため、それを記入していきます。

```jsx:src/reducers/auth.js
const initialState = {
  displayName: null
}

const auth = (state=initialState, action) => {
  switch (action.type) {
  //actionから送られてきたtypeの内容によって処理を分ける

    case 'LOGIN': {
      return Object.assign({}, state, {
        displayName: action.payload.displayName
      })
    }
　　//action.type = LOGIN だったら、this.state.displayNameにaction.payload.displayNameをコピーする

    case 'LOGOUT': {
      return Object.assign({}, state, {
        displayName: null
      })
    }
    //action.type = LOGOUT だったら、displayNameにnullを入れる
   
    default: {
      return state
    }
  }
}

export default auth
```

簡単な**Reducer**を定義したため、もっと詳しく知りたい方は下記の記事を参考にしてみてください。

- [Redux入門 3日目 Reduxの基本・Reducers(公式ドキュメント和訳)](https://qiita.com/kiita312/items/7fdce94912d6d9c801f8)


#### src/containers/ChatFormContainer.js
**container**の作成をしていきます。
`View`を設定するReact部分に渡す`state`を定義したり、であるユーザーが行った操作毎に**Redux**に情報を渡したりします。
コメントアウトでファイル内の各コードを説明していきます。

```jsx:src/containers/ChatFormCotainer.js
import firebase from 'firebase/app'
import { connect } from 'react-redux'
import { loginAction,logoutAction } from '../actions/Auth'
import ChatForm from '../components/ChatForm'

const mapStateToProps = (state) => {
  console.log(state.auth.displayName)
  return {
    displayName: state.auth.displayName,
  }
}
//ChatFormで使用するdisplayNameの設定

const mapDispatchToProps = (dispatch) => {
//stateを更新するイベントを記述
  return {
    Login(){
      let provider = new firebase.auth.GoogleAuthProvider()
      firebase.auth().signInWithPopup(provider)
    },
    //firebaseに接続してPopupを使用してログインする

    refLogin(){
      firebase.auth().onAuthStateChanged(user => {
        if (!user) {
          return
        }
       dispatch(loginAction(user))
      })
    },
　　//ログインしたらuserをfirebaseの情報に更新してstateを更新する

    Logout(){
      firebase.auth().signOut()
      dispatch(logoutAction())
    }
    //firebaseからログアウトする
  }
}

const LogoutStateFormContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(ChatForm)

export default LogoutStateFormContainer
```


#### src/components/ChatForm.js
`ChatFormContainer`の作成が終了したら、`src/components/ChatForm.js`を編集します。
やることは**`ChatFormContainer.js`に記入したイベントの削除と`Redux`で管理している`state`の読み込みです。**

```jsx:src/components/ChatForm.js
import React,{Component} from 'react'
import firebase from 'firebase/app'
import {firebaseApp} from '../firebase/firebase'

class ChatForm extends Component {
  constructor(props){
    super(props)
  }
  componentDidMount(){
    this.props.refLogin()
  }
  render(){
    return(
      <div id='Form'>
        {this.props.displayName ?
          <div className='login'>
            <h2>{this.props.displayName}</h2>
              <div className='buttons'>
                <button name='user_name' value={this.props.displayName} onClick={this.props.onTextChange}>この名前を使う</button>
                <button onClick={this.props.Logout}>違う名前を使う</button>
              </div>
          </div> :
          <div className='logout'>
            <input name='user_name' onChange={this.props.onTextChange} placeholder='名前'/>
            <button onClick={this.props.Login}>Goggle Login</button>
          </div>
        }
        <textarea name='text' onChange={this.props.onTextChange}  placeholder='メッセージ'/>
        <button className='send' onClick={this.props.onButtonClick}>送信</button>
      </div>
    )
  }
}

export default ChatForm
```

イベントのほとんどが削除され、`View`に関するファイルにすることができました。
`this.props.displayName`で**Redux**が管理している`state`を読み込んでいます。

以上でコード、ファイルの操作は終了です。


#### Git commit
[feat: Create ChatFormContainer](https://github.com/kaibara/React-chat/pull/24/commits/8b92c98848240a20c119c8032ae8bfd58ba2a52c)

### redux-devtools-extensionの導入
**Redux**を使用するアプリ、システムを作成される場合は、**redux-devtools-extension**の導入をオススメします。

**[redux-devtools-extensions](https://github.com/zalmoxisus/redux-devtools-extension#usage)**

これはブラウザの画面から`state`を管理している**Redux**の**Store**を確認することができるツールです。

[Githubのドキュメント](https://github.com/zalmoxisus/redux-devtools-extension#usage)から各自が使用しているブラウザに合わせた設定を行い、`createStore`を使用しているコードを次のようにするだけで使用できるようになります。

```jsx:src/index.js
const store = createStore(Reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
```

今回のアプリではログイン時、ログアウト時の`state`の変化が次のように表示されます。

**ログイン時**
<img src="https://qiita-image-store.s3.amazonaws.com/0/218398/d7229c4f-b17f-33cf-ca78-bccbc248c1a4.png" width="900px"> 

**ログアウト時**
<img src="https://qiita-image-store.s3.amazonaws.com/0/218398/421c4450-8725-59fc-66b0-47d6b9c7dd69.png" width="900px"> 


**redux-devtools-extensions**を使って皆さんの開発を進めてください。

## まとめ
今回はFirebaseでログイン管理しているReactアプリに**Redux**を導入する手順を紹介しました。
正直な話、このやり方は**Redux**を導入にのみ焦点を充てているため、勉強には適していないと思います。
自分のようにファイルを往復するよりも、**Action -> Reducer -> Store**と流れに沿ったやり方が一番理解に近づくのではないでしょうか。
この記事を基に勉強する場合は、とりあえずコードだけを見て、書いて、それから公式ドキュメントと照らし合わせて、実際の流れをイメージしながら進めていくことをオススメします。


## 参考資料
[react+reduxで非同期処理を含むtodoアプリを作ってみる](https://qiita.com/halhide/items/a45c7a1d5f949596e17d#redux)
[Reduxの実装とReactとの連携を超シンプルなサンプルを使って解説](https://mae.chab.in/archives/2885#post2885-1)
[React+Redux+Firebase Authenticationでログイン/ログアウト機能を実装する (1)](https://qiita.com/k5trismegistus/items/2b82c9721e5d245b54ca)
[React+Redux+Firebase Authenticationでログイン/ログアウト機能を実装する (2)](https://qiita.com/k5trismegistus/items/7aa3b61d3815a4474016)
[Redux入門 1~6日目 by Redux入門【ダイジェスト版】10分で理解するReduxの基礎](https://qiita.com/kiita312/items/49a1f03445b19cf407b7)
[redux-devtools-extensions](https://github.com/zalmoxisus/redux-devtools-extension#usage)