この記事は ドワンゴ2021年アドベントカレンダー 12/9の記事です。
昨日は @hmiyado さんの 3年間のコーディング規約の変遷に見る Kotlin の変化 でした。
なにこの記事
FirestoreのデータをReduxに反映する redux-firestore-hooks を作ったのでどんなものか紹介します。
FrestoreとReact, Reduxを使っている際にシンプルで効果的な手助けになるかもしれません。
FirestoreとReduxを使う際の考え方
Firestore はドキュメント指向のデータベースで、クライアントのSDKを使うことで簡単にデータの書き込みと読み取り、サブスクライブができます。
Redux はアプリケーションの状態を管理するためのライブラリです。ActionをDispatchすることで状態を更新し、状態をサブスクライブすることでシームレスにViewに反映したりできます。
今回はFirestoreからサブスクライブしたデータをReduxに反映させたいという一般的な設計について考えます。
以下のような流れになるでしょう。
①アプリケーションはFirestoreからデータをサブスクライブし、データの更新を検知します。
②データの更新をもとにActionをDispatchし、ReduxのStateが更新されます。
③アプリケーションはReduxのstateをサブスクライブしているので、stateが更新されるとそれに応じてReactのビューが更新されたりします。
redux-firestore-hooksがすること
- Firestoreをサブスクライブする際のイベントリスナー関数を作るHooksの提供
- イベントリスナーは受け取ったデータをもとにActionをdispatchをし、stateを更新する
- そのActionを受けつけStateを更新するためのReducerを提供
主にこの2種類のシンプルな関数を提供しています。
使い方はこのような感じです。
store.ts
// Redux Toolkit の書き方で書いているが他の書き方でもok
import { configureStore } from '@reduxjs/toolkit'
import { createReducer } from 'redux-firestore-hooks'
// Firestore の ドキュメントの型
export type User = {
displayName: string
photoURL: string
}
// Firestore の ドキュメントの型
export type Chat = {
userId: string
text: string
}
// idにはドキュメントのidが入る
type FirestoreState = {
users?: { [id in string]: User }
chats?: { [id in string]: Chat }
}
const store = configureStore({
reducer: {
// firestore stateの型を効かすにはここで型を入れる。
firestore: createReducer<FirestoreState>(),
},
})
export type RootState = ReturnType<typeof store.getState>
export default store
App.tsx
// App.tsx
import { collection, doc, onSnapshot, query } from 'firebase/firestore'
import { useDispatch, useSelector } from 'react-redux'
import { useApplyCollection, useApplyDocument, clear } from 'redux-firestore-hooks'
const App = ({ userId }) => {
const dispatch = useDispatch()
const applyDocument = useApplyDocument(dispatch)
const applyCollection = useApplyCollection(dispatch)
// Appマウント時にchatsコレクションをサブスクライブしfirestore.chats stateに反映させる
useEffect(() => {
const unsubscribeChats = onSnapshot(query(collection(db, 'chats')), applyCollection('chats'))
return () => {
unsubscribeChats()
// clear chats data
dispatch(clear('chats'))
}
}, [applyCollection])
// userIdに応じてusersドキュメントをサブスクライブしてfirestore.users stateに反映させる
useEffect(() => {
const unsubscribeUser = onSnapshot(doc(db, `users/${userId}`), applyDocument('users'))
return () => {
unsubscribe()
// clear user data by userId
dispatch(clear(['users', userId]))
}
}, [userId, applyDocument])
return null
}
Appコンポーネントがマウントされ、Firestoreへのサブスクライブが完了したときのReduxのstateは以下のようになります。
{
firestore: {
users: {
xj0cjs: {
displayName: 'Alice'
photoURL: 'https://example.com/alice.png'
},
}
chats: {
fajei8: {
userId: 'xj0cjs',
text: 'こんにちは、Bob',
},
d8cjs2: {
userId: 'f82bma',
text: 'こんにちは、Alice',
}
}
}
}
このように簡単にFirestoreのデータをReduxに反映させることができます。
redux-firestore-hooksの良いところ
Firestore のデータをReduxに反映させるためのツールとして、 redux-firestore を使うという別の手があります。 ( React と一緒に使うなら react-redux-firebase 経由で使われます )
redux-firestoreも良い仕組みだとは思うのですが、これはFirebase SDKのI/Fをラップして新しいAPIを利用者に使わせてしまうので、学習コストが高いです。また、バンドルサイズも増えます。
それに比べて redux-firestore-hooks は「Firestoreのイベントリスナー関数を生成する」という手法を取ることによって、素のFirebase SDKのI/Fを使ったままReduxにstateを反映させることができます。
whereクエリ、リスナーのデタッチ、 リッスンエラーの処理など、Firebase SDK本来のI/Fをそのまま使えます。
また、以下のように書けばリスナー関数でReduxに反映する以外の処理も追加で書くことが可能です。
onSnapshot(query(collection(db, 'chats')), (querySnapshot) => {
applyCollection('chats')(querySnapshot)
doOtherThingWith(querySnapshot)
})
このように、Firebase SDKをラップしたredux-firestoreと違い、素のFirebase SDKのI/Fを使えることで柔軟かつシンプルな設計になります。
まとめると、シンプルイズベスト、小さなものを作ってうまく協調するようにする、そんな思想が強い人におすすめです。
おわりに
アドベントカレンダー用に締め切り駆動開発的にライブラリを作ったのでまだ荒削りです。
なにかバグがあったらissueまでお願いします。感想はこのコメント欄や @macinjoke にくれるとありがたいです。
まとめ
- FirestoreのデータをReduxに反映するための redux-firestore-hooks を作った。
- シンプルさを求めたい人におすすめ
ついでに一昨年のドワンゴアドカレに書いた記事も置いときます。