React(Hooks)で認証付きリアルタイムチャットアプリを作ってみた。

# はじめに
Reactの勉強を始めて、Hooksを使ってchatアプリを作成してみました。
一度作ってからこの記事を作成したので、説明する順序がおかしいと感じる点があるかもしれません。
特に注意して作成した箇所をまとめてみたので全体のコードを確認したい場合は
ここにコード置いています。
https://github.com/m-shichida/chat-app
(記事のコードは過去のものですが、現在のコードは変更しました。)
作成途中ですが、最低限の

- ログインユーザーの管理
- メッセージの送受信、表示

はできています。
firebaseの設定等は割愛します。

- React(Hooks)
- firebase
- Typescript

使いました。
修正点あれば教えてください。

## 下準備
まずこのチャットアプリでは

ログインしている状態をログイン画面以外のコンポーネントに使用するため`createContext`, `useContext`を使って

そのため、`components/App.js`（`components`のルートファイル）に

- ①`state`の初期値
- ②`reducers`内のstateを変更させる`dispatch`を使用するとの宣言
- ③全ての階層でstate/dispatchが使用できるようにする。

この３つを記述しました。

```components/App.js
function App() {
  // ①stateの初期値の設定
  const initialState = {
    currentUserInfos: '',
    chatRooms: [],
    messages: []
  }

  // ②reducers内のstateを変更させる`dispatch`を使用するとの宣言
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    firebaseDb.ref('messages/').on('child_added', (snapshot) => {
      const messages = snapshot.val()
      dispatch({
        type: SET_MESSAGES,
        messages
      })
    });
  }, [])

  useEffect(() => {
    firebaseApp.auth().onAuthStateChanged((user) => {
      if (user) {
        const uid = user.uid
        const name = `ゲストユーザー${ uid }`
        const image = user.photoURL

        dispatch({
          type: ADD_CURRENT_USER_INFO,
          uid,
          name,
          image
        })
      }
    });
  }, []);

  // ③全ての階層でstate/dispatchが使用できるようにする。
  return (
    <AppContext.Provider value={ { state, dispatch } }>
      <BrowserRouter>
        { state.currentUserInfos ? (<Routes />) : (<Login />) }
      </BrowserRouter>
    </AppContext.Provider>
  );
}

export default App;
```

そして複数のstate/dispatchを管理できるようにするためにReduxの`combineReducers`を使用します。

```reducers/index.js
import { combineReducers } from 'redux';
import currentUserInfos from './currentUserInfos';
import messages from './messages';

// 複数のstateとdispatchを管理することが可能になる。
export default combineReducers({
  messages,
  currentUserInfos
})
```

# ログイン状態を保持する

```components/login.js
import React, { useContext } from 'react';
import { AppContext } from '../../contexts';
import { Button, Card, CardActions, CardContent, Grid } from '@material-ui/core';
import firebase from 'firebase';
import { firebaseApp, firebaseDb } from '../../firebase';
import { ADD_CURRENT_USER_INFO } from '../../actions';

const Login = () => {
  // AppContextからstateの状態を変更させるdispatchを呼び出す。
  const { dispatch } = useContext(AppContext);
  
  //匿名アカウントでログイン
  const loginAsAnonymousUser = e => {
    e.preventDefault();
    firebaseApp.auth().signInAnonymously().catch(function(error) {
      const errorCode = error.code;
      const errorMessage = error.message;
      alert(`エラーが発生しました。エラーコード${ errorCode }:${ errorMessage }`)
    });

    firebaseApp.auth().onAuthStateChanged(function(user) {
      if (user) {
        const uid = user.uid;
        const name = `ゲストユーザー${ uid }`
        dispatch({
          type: ADD_CURRENT_USER_INFO,
          uid,
          name
        });
        alert(`${ name }としてログインしました。`)
      }
    });
  }
  // Googleアカウントでログイン
  const loginAsGoogleAccount = () => {
    let provider = new firebase.auth.GoogleAuthProvider();
    firebaseApp.auth().signInWithPopup(provider).then(function(result) {
      const user = result.user;
      const uid = user.uid
      const name = user.displayName;
      const image = user.photoURL
      const params = { uid, name, image }
      dispatch({
        type: ADD_CURRENT_USER_INFO,
        uid,
        name,
        image,
      })
    }).catch(function(error) {
      const errorCode = error.code;
      const errorMessage = error.message;
      alert(`エラーが発生しました。エラーコード${ errorCode }:${ errorMessage }`)
    });
  }

  return (
    <Grid
      container
      justify='center'
      style={ { position: 'fixed', top: '35%' } }
    >
      <Card style={ { width: '300px' } }>
        <CardContent>
          <h3>ログインして利用する</h3>
        </CardContent>
        <CardActions style={ { display: 'flex', flexDirection: 'column' } }>
          <Button
            className='loginBtn'
            variant='contained'
            color='primary'
            onClick={ loginAsAnonymousUser }
            style={ { marginBottom: '8px' } }
          >
            匿名ログイン
          </Button>
          <img
            className='loginBtn'
            alt='GoogleLoginImage'
            style={ { cursor: 'pointer' } }
            src={ `${ process.env.PUBLIC_URL }/images/btn_google_signin.png` }
            onClick={ loginAsGoogleAccount }
          />
        </CardActions>
      </Card>
    </Grid>
  )
};

export default Login;
```

匿名ログイン、グーグルアカウントを利用してのログイン両方とも、ログインに成功してもstate管理しなければ、ログイン状態を保持することができないため、```dispatch```を使ってstate管理します。

またログイン後にページをリロードしてしまうとこれもまたログイン情報を管理したstateが全てなくなってしまうので、`useEffect`を使ってページリロード時にログインデータをfirebaseから持ってきます。

```App.js
useEffect(() => {
  firebaseApp.auth().onAuthStateChanged((user) => {
    if (user) {
      const uid = user.uid
      const name = `ゲストユーザー${ uid }`
      const image = user.photoURL

      dispatch({
        type: ADD_CURRENT_USER_INFO,
        uid,
        name,
        image
      })
    }
  });
}, []);
```

```login.js
dispatch({
  type: ADD_CURRENT_USER_INFO,
  uid,
  name,
  image // 匿名ログインのときはいらないです。
})
```
このdispatchが以下の```currentUserInfos```に渡ります。state/dispatchが一つだけの管理で済むのであれば、格納場所は```reducers/index.js```で大丈夫です。

```reducers/currentUserInfos.js
import { ADD_CURRENT_USER_INFO, DELETE_CURRENT_USER_INFO } from '../actions';

const currentUserInfos = (state = [], action) => {
  switch(action.type) {
    case ADD_CURRENT_USER_INFO:
      let image = action.image ? action.image : '';
      let currentUserInfos = { uid: action.uid, name: action.name, image }
      return currentUserInfos
    case DELETE_CURRENT_USER_INFO:
      return '' 
    default:
      return state
  }
}

export default currentUserInfos;
```

# メッセージを送信する
次に```messages.js```。
dispatch内でstate管理をするとともに、firebaseへメッセージの情報を格納しました。

```reducers/messages.js
import { ADD_MESSAGE, SET_MESSAGES } from '../actions'
import currentDate from '../shared';
import { firebaseDb } from '../firebase';

const messages = (state = [], action) => {
  switch(action.type) {
    case ADD_MESSAGE:
      // state管理する
      const message = { uid: action.uid,
                        userImage: action.image,
                        content: action.content,
                        createdAt: action.createdAt ? action.createdAt : currentDate() }
      // firebaseにメッセージ情報を保存する。
      firebaseDb.ref('messages/').push(message);
      return [...state, { ...message }]
    case SET_MESSAGES:
      return [...state, action.messages]
    default:
      return state
  }
}

export default messages;
```

# データに変更があった時
firebaseではデータに変更があった場合に

```
firebaseDb.ref('messages/').on('child_added', (snapshot)
```

でリアルタイムでデータを取ってくることができます。

```components/App.js
// あとで出てくると書いてあったとこ。
// メッセージのデータに変更があった場合、'初回の一回だけ'stateを更新する。
  useEffect(() => {
    firebaseDb.ref('messages/').on('child_added', (snapshot) => {
      const messages = snapshot.val()
      dispatch({
        type: SET_MESSAGES,
        messages
      })
    });
  }, [])
```
`components/Messages.js`(複数メッセージを表示させるコンポーネント)にこれを書くのかな、と思ったのですが、ボトムナビゲーションなどを使用している時にページ切り替えのたびに`Messages`コンポーネントが呼ばれることになり、何度も重なってメッセージが呼ばれることになります。
そのため、一回だけ呼び出させるコンポーネント、`components/App.js`にこれを定義しました。

**終わり**