1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Suspenseでローディング画面表示を回避しつつデータを即時反映させる方法

Posted at

はじめに

前回の記事で登録したデータの即時反映はできたのですが、登録の度にローディング画面が出てくるのはユーザー体験としてよくないな、と感じ、削除機能の実装と併せてここも改善することにしました。

やりたかったこと

記録の追加/削除時になるべくローディング画面を表示することなく、画面に即時反映したい。
StudyRecordApp_listRefresh.gif

解決策

データ追加時

以前の再取得用関数は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等を使うべきなのでしょうが、上手くいかずこのような実装になりました。もっといい方法があればご指摘頂けると幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?