はじめに
前回の記事で登録したデータの即時反映はできたのですが、登録の度にローディング画面が出てくるのはユーザー体験としてよくないな、と感じ、削除機能の実装と併せてここも改善することにしました。
やりたかったこと
記録の追加/削除時になるべくローディング画面を表示することなく、画面に即時反映したい。
解決策
データ追加時
以前の再取得用関数はPromiseインスタンスのキャッシュクリアのみ行い、Suspense
の性質を利用して再度データを取得してくるものでした。
データ取得フック
src/hooks/useFetchData.tsx
import { use } from "react";
import { GetRecords } from "../lib/record";
import { Record } from "../domain/record";
let recordsPromise: Promise<Record[]> | null = null;
export const useFetchData = () => {
if (!recordsPromise) {
recordsPromise = new Promise<Record[]>((resolve) => {
(async () => {
const data = await GetRecords();
resolve(data);
})();
});
}
const records = use(recordsPromise);
return { records };
};
export const refetchRecords = () => {
recordsPromise = null;
};
Supabase側処理
src/lib/record.ts
import { Record } from "../domain/record";
import { supabase } from "../utils/supabase";
export async function GetRecords(): Promise<Record[]> {
const response = await supabase.from("study-record").select("*");
if (response.error) {
throw new Error(response.error.message);
}
const recordsData = response.data.map((record) => {
return new Record(record.id, record.title, record.time);
});
return recordsData;
}
export async function AddRecord(
title: string,
time: number
): Promise<Record[]> {
await supabase
.from("study-record")
.insert([
{
title: title,
time: time,
},
])
.select();
return GetRecords();
}
export async function DeleteRecord(id: string): Promise<Record[]> {
await supabase.from("study-record").delete().eq("id", id);
return GetRecords();
}
そこで、データ追加時にPromise.resolve
を渡し、これをフォームの登録時に渡すようにしました。
src/hooks/useFetchData.tsx
const updateRecords = (newRecords: Record[]) => {
recordsPromise = null;
recordsPromise = Promise.resolve(newRecords);
};
return { records, setData: updateRecords };
src/components/InputForm.tsx
const onClickAdd = async () => {
try {
const newRecords = await AddRecord(title, time);
setData(newRecords);
reset({ title: "", time: 0 });
onClose();
} catch (error) {
console.error(error);
}
};
use
は既に解決済みのPromise
を受け取った場合、サスペンドせず、即座に値を返す性質があります。
AddRecord()
を呼び出すときに既にSupabaseから最新の一覧は受け取っているので、これによってLoading画面が表示されることなく、追加したデータが表示されるようになりました。
データ削除時
削除の場合はsetData
を呼び出すのみだと上手く動作しなかったので、App.tsxに再マウント用の関数を用意し、削除ボタンを押すとそれを呼び出すようにすることで、削除結果が即時反映されるようになりました。
App.tsx
const [refreshKey, setRefreshKey] = useState(0);
const refreshData = () => {
setRefreshKey((prev) => prev + 1);
};
return (
<>
<Suspense
fallback={
<Heading as="h2" size="md">
Loading...
</Heading>
}
>
<RecordList key={refreshKey} onDataChange={refreshData} />
</Suspense>
</>
);
}
src/components/RecordList.tsx
const onClickDelete = async (id: string) => {
const updateRecords = await DeleteRecord(id);
setData(updateRecords);
onDataChange();
};
return <Button onClick={() => onClickDelete(record.id)}>削除</Button>
おわりに
本来ならばstartTransition
等を使うべきなのでしょうが、上手くいかずこのような実装になりました。もっといい方法があればご指摘頂けると幸いです。