はじめに
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が使用できるようにする。
この3つを記述しました。
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
を使用します。
import { combineReducers } from 'redux';
import currentUserInfos from './currentUserInfos';
import messages from './messages';
// 複数のstateとdispatchを管理することが可能になる。
export default combineReducers({
messages,
currentUserInfos
})
ログイン状態を保持する
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から持ってきます。
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
})
}
});
}, []);
dispatch({
type: ADD_CURRENT_USER_INFO,
uid,
name,
image // 匿名ログインのときはいらないです。
})
このdispatchが以下のcurrentUserInfos
に渡ります。state/dispatchが一つだけの管理で済むのであれば、格納場所はreducers/index.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へメッセージの情報を格納しました。
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)
でリアルタイムでデータを取ってくることができます。
// あとで出てくると書いてあったとこ。
// メッセージのデータに変更があった場合、'初回の一回だけ'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
にこれを定義しました。
終わり