12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-18

はじめに

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

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

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

  • React(Hooks)
  • firebase
  • Typescript

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

下準備

まずこのチャットアプリでは

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

そのため、components/App.jscomponentsのルートファイル)に

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

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

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にこれを定義しました。

終わり

12
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?