背景
React, TypeScript, Firebaseの個人開発を進めるにあたって、Firebaseで型定義ってどうやってやるんだ?と疑問に思い調査したら公式がwithConverterを用意していることを知りました。
公式の出しているwithConverterのコードを使い続けると肥大化するばかりだったのでいい感じに使いまわせるファイルとして切り出したのでメモします。
実装までの流れ
呼び出し元で型とパスを指定すれば結果(documents)とエラー(error)を取得することができます。
useCollection以下をわざわざ見に行かなくても感覚的に使えるようになっているのではないでしょうか?今回はuseCollectionを例に解説をしていきます。
実装したコード(converter)
Partialでさらに型を緩くすることもできます。
※Partialは型の持つ全部のキーを省略可能にします。
import { firebase, projectFirestore } from "../firebase/config";
const converter = <T>() => ({
// NOTE:toFirestore: (data: Partial<T>) => data,で曖昧にすることもできる
toFirestore: (data: T) => data,
fromFirestore: (snap: firebase.firestore.QueryDocumentSnapshot) =>
snap.data() as T,
});
const collectionPoint = <T>(collection: string) =>
projectFirestore.collection(collection).withConverter(converter<T>());
const documentPoint = <T>(collection: string, document: string) =>
projectFirestore
.collection(collection)
.withConverter(converter<T>())
.doc(document);
const subCollectionPoint = <T, U>(
collection: string,
document: string,
subCollection: string
) =>
projectFirestore
.collection(collection)
.withConverter(converter<T>())
.doc(document)
.collection(subCollection)
.withConverter(converter<U>());
const subDocumentPoint = <T, U>(
collection: string,
document: string,
subCollection: string,
subDocument: string
) =>
projectFirestore
.collection(collection)
.withConverter(converter<T>())
.doc(document)
.collection(subCollection)
.withConverter(converter<U>())
.doc(subDocument);
export {
collectionPoint,
documentPoint,
subCollectionPoint,
subDocumentPoint,
}
実装したコード(useCollection)
converterを呼んでいるのがcollectionだけをまとめた層になります。
ここでFirestoreへアクセスしデータを取ってきています。
import { useEffect, useState, useRef } from "react";
import { firebase } from "../firebase/config";
import { collectionPoint } from "../utilities/converter";
import { firebasePath } from "../@types/dashboard";
export const useCollection = <T,>(
{ collection }: firebasePath,
_query?: [string, WhereFilterOp, any],
_orderBy?: [string, OrderByDirection]
) => {
const [documents, setDocuments] = useState<Array<T>>([]);
const [error, setError] = useState<string | null>(null);
const query = useRef(_query).current;
const orderBy = useRef(_orderBy).current;
useEffect(() => {
let ref = collectionPoint<T>(collection);
if (query) {
ref = ref.where(...query) as firebase.firestore.CollectionReference<T>;
}
if (orderBy) {
ref = ref.orderBy(
...orderBy
) as firebase.firestore.CollectionReference<T>;
}
const unsubscribe = ref.onSnapshot(
(snapshot) => {
const results: Array<T> = snapshot.docs.map((doc) => {
return { ...doc.data(), id: doc.id };
});
setDocuments(results);
setError(null);
},
(error) => {
setError("データを取得できませんでした。");
}
);
return () => unsubscribe();
}, [collection, query, orderBy]);
return { documents, error };
};
実装したコード(呼び出し元)
const { error, documents } = useCollection<User>(convertedPath("/users"));
pagesやcomponentsで呼び出します。
pathをコンバートして文字列からオブジェクトにしていますが、なぜこのような仕様にしているかというと、自分ではなく他の人が実装する時に文字列の方が見やすく、どのコレクションにアクセスしているかわかりやすいのかなと思ったからです。(単純にコードの行数が増えるのを避ける目的もありますが…)
さいごに
converterのファイルを切り出した時「あれ、型定義ってどうやって投げるんだ?」と悩んだり、クエリの型定義をArray<string>
にしてしまってエラーが止まらなかったりとTypeScriptの理解の部分で躓きました。
実務でTypeScriptを使ってはいますが如何に雰囲気で使っていたのかを実感。
一方でジェネリクスについてのユースケースやFirebaseの提供している型定義ファイルにも目を通すきっかけにもなったので、個人開発を通してわからないこと、アンチパターンや地雷を踏んで理解を深めるやり方が大切だなあと思いました。
参考URL