Redux Toolkitがすごい便利だった
画面遷移しても状態保存しておこうと思い、再度reduxを見直していたが、これを見つけてちょっと感動した。
reduxの構成にしたい人はこれ入れておくとよさそう。
action, reducer, selectorの記述が少ない
reduxは何と言ってもコードの記述量が増えるのがめんどくさいのだが、とてもスマートに書ける。
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { CourtDoc } from 'models/court' import type { RootState } from 'store' type Mode = 'text' | 'location' type Geo = { lat: number lng: number } interface HomeState { mode: Mode zoom: number center: Geo courts?: CourtDoc[] } const initialState: HomeState = { mode: 'text', zoom: 11, center: { lat: 35.681236, lng: 139.767125 }, } export const homeSlice = createSlice({ name: 'home', initialState, reducers: { changeMode: (state, action: PayloadAction<Mode>) => { state.mode = action.payload }, changeZoom: (state, action: PayloadAction<number>) => { state.zoom = action.payload }, changeCenter: (state, action: PayloadAction<Geo>) => { state.center = action.payload }, }, }) export const { changeMode, changeZoom, changeCenter } = homeSlice.actions export const selectMode = (state: RootState): Mode => state.home.mode export const selectZoom = (state: RootState): number => state.home.zoom export const selectCenter = (state: RootState): Geo => state.home.center export const selectCourtDocs = (state: RootState): CourtDoc[] | undefined => state.home.courts export default homeSlice.reducer
actionのstringを自分で書かなくても、 createSlice
のname
と関数名から自動でいい感じに生成してくれる。
reducers
の書き換えをしてもglobalに影響が出ないようになっている。
reselect
まだやってないけど createSelector
でSelectorを作ると reselect
が使われてパフォーマンス上がる模様。
createSelector | Redux Toolkit
TypeScript対応
型がちゃんと付く。
フラグ一個でredux-devtools-extensionがつく
export const store = configureStore({ reducer: { firebase: firebaseReducer, firestore: firestoreReducer, home: HomeReducer, }, devTools: true, // <- これだけ })
副作用の対応
前述のcreateSlice
のところで、
import { createAsyncThunk, ... } from '@reduxjs/toolkit' import { search } from 'models/search' ... export const searchByText = createAsyncThunk( 'home/searchByText', async (params: { text: string; hits: number }) => { const courtDocs = await search(params.text, params.hits) return courtDocs } ) export const homeSlice = createSlice({ ..., extraReducers: { [searchByText.fulfilled.type]: ( state, action: PayloadAction<CourtDoc[]> ) => { state.courts = action.payload }, }, }) ... export default homeSlice.reducer
これでいい。
すごい便利だ...。まだかじっただけなので、もうちょっと調べる。
google-map-reactから@react-google-maps/apiに乗り換え
もともと google-map-react
を使っていたのを @react-google-maps/api
に変更。
google-map-react
はダウンロード数が多く、当初必要だった要件を満たしそうだったので導入したものの、デフォルトのマーカーへの変更を考えたときに、
new maps.Marker({ position: { lat, lng } map, })
のようにライブラリを直接呼び出す必要があり、Reactっぽくなく使いにくかった。 クリックイベントも取得しにくいので、他ライブラリを調査することに。
google-maps-react というそっくりな名前のパッケージもあったが、あまりメンテされてなさそうなので不採用。
結局こちらを使うことに。
<GoogleMap mapContainerStyle={{ width: '100%', height: '100%', }} onIdle={onMapMoved} onLoad={onLoaded} > {courtDocs.map((courtDoc) => { return ( <Marker key={id} position={{ lat, lng }} onClick={() => onClickMarker(id)} /> ) })} </GoogleMap>
これなら Marker
をReact Componentとして扱える。
吹き出しは InfoWindow
のコンポーネントを使う。
これも Marker
と同じように GoogleMap
の子として追加すればよい。
吹き出しは✗で消せるので、消えたときに InfoWindow
も取り除かないと再表示がうまくいかなかった。
23区分だけデータ入れ終わった
公私共に忙しかったので進捗悪かったけど、とりあえず23区だけ。 市区町村入れるとあと44ありそう...。
ただここでデータの形式についてちょっと考えたいかもしれない。 特に料金が複雑で検索パラメータとして入れにくい。
どう探したいかを考えながらデータ決めるといいだろうか。
自作サービスにデータを入れていっている
都内のテニスコートを探してはデータ入力していっている。
改めて思ったのはテニスコートの料金って結構複雑であるということ。
- 時間帯
- 季節
- コート
- 地域住民かどうか
- 照明利用有りか
などで料金は変わるし、予約できる時間単位(1時間、2時間など)も重要。
設計段階で予想はされていたので、一旦乱暴に文字列で突っ込んでいる状況(URL参照、みたいのもある...)だが、一通りデータ入力し終わったら、料金のデータ構造どうするか再考したいところ。自分がユーザなら料金と地域住民じゃなくても借りられるかを検索フィルタできるのは結構嬉しい。
あとはデータのメンテナンスはどうするか。当然自分だけでは監視しきれないので、ユーザによる報告か直接編集で対応する感じかな。最初は報告ベースかな。
Firestoreの住所のfieldを整理
Firestoreのルールのテストを書いた
Firestore rulesのテストを書いていく。
initializeAdminAppで取得したappでfirestoreに書き込む際、Timestampの型はfirebase-admin由来でないとエラーになった。
Value for argument "data" is not a valid Firestore document. Detected an object of type "Timestamp" that doesn't match the expected instance (found in field "createdAt"). Please ensure that the Firestore types you are using are from the same NPM package.)
Timestampを@firebase/rules-unit-testingではなくfirebase-adminから型を取ったら直った。
次のエラー。
(node:18029) UnhandledPromiseRejectionWarning: Error: FIRESTORE (8.2.5) INTERNAL ASSERTION FAILED: Unexpected state (node:18029) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 33)
jest --env=node
にする。
また、作成したapp(adminもそうでないのも)は都度delete()で消さないとCI上だとjestが終了しなくなる。
jest --forceExit
を使うと終了させることができるが、気持ち悪いなら都度掃除する。
テストコードの最初はこんな感じになった。
import * as firebase from '@firebase/rules-unit-testing' import * as admin from 'firebase-admin' const projectId = 'rules-courts-test' beforeEach(async () => { firebase.loadFirestoreRules({ projectId, rules: fs.readFileSync('firestore.rules', 'utf8'), }) const adminApp = firebase.initializeAdminApp({ projectId }) // adminAppを使ってテストの初期状態となるデータを書き込んでいく await adminApp.delete() }) afterEach(async () => { await firebase.clearFirestoreData({ projectId }) })
また、公式ガイドだとassertSucceeds, assertFailsはawait無しで書いてあるので、もしやいらない?と思ったが、awaitしないと正しいテスト結果にならなかった。
test('read shoud be succeeded', async () => { await firebase.assertSucceeds( app.firestore().collection('courts').doc('Slsnk6XjulO3ndipFjlY').get() ) })
Youtubeでもawait入れてるしよさそう。
実際の差分はこうなった。
Firestoreルールのテストの書き方を再調査
Firestoreルールのテストの書き方を復習がてら再調査。
カバレッジの取得。そういえばできた気がする。
http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html
Firestoreに限らないかもしれないが、最初のデータセットアップが結構めんどくさい。 本家にもAdminAppを使うぐらいしか解を書いていない。
テストのセットアップが非常に複雑である
予め作った、データセットが定義されたファイルからインポートできたらいいのになぁと思っているのだが...
ちなみにテスト前にデータのimportはできるかもしれない。が、beforeEachで都度インポート、みたいなことはできなそう。 firebase.google.com
いろいろ調べたが、やはり都度AdminAppでやるしかなさそう。
基本的には*.test.tsごとにプロジェクトIDを指定し、beforeEachでデータセットアップ、afterEachでclearFirestoreDataするようにしたい。 beforeEachごとにプロジェクトのIDを更新するというのが前あったが、使い捨てプロジェクトがガンガン生成されるためちょっときれいじゃない感がある。他に方法なさそうならこれでもいいけど。
目的と直接関係ないが@firebase/rules-unit-testingにこんなのがあった。
withFunctionTriggersDisabled
この引数のクロージャの中でFirestoreを書き換える分にはFirestore Triggerが発火しないらしい。でも実装見てる感じ、クロージャの中実行前後でエミュレータのWeb API叩いて全体でdisableさせてるみたいなので、並列でテストが実行されていたりするとおかしなことになりそう。