Supabaseのリアルタイムリスナーを使ってToDoアプリ作ってみた
- 作成日:2023/03/26
目次
タイトルの通り、Supabaseのリアルタイムリスナーを使ってToDoアプリを作成してみました。Next.jsで作成しています。
Supabase今後ますますシェアを伸ばしていくと予想されるBaasの1つです。
ちなみに読み方は「スーパベース」らしい。スパベースだと思ってた。。
今回は、Supabaseとは何か?の説明は省きます。
公式ドキュメントや、Discordが充実していますのでぜひ参考にしてみてください!
背景
なぜSupabaseを使用したかというと、
・FirebaseのCloud FirestoreはNoSQLなので、NoSQLを卒業して(?)RDBに慣れたい
・GUIで操作も可能なので、テーブル設計から徐々にPostgreSQLも触れるようになりたい
・Next.jsでの開発が主なので、得意なBaaSを持っておいてサクッとDB周りを開発してフロントに専念したい
などの理由があるからです。
ちなみに、PlanetScaleも少しかじりましたが、自分にはまだ早いようでした🥲
Node.js周りも学習してから再度チャレンジ予定です。
Supabase側の設定
SupabaseはGUIで直感的にDBの作成・操作をすることが可能です。
管理画面の操作についてはたくさん記事が出回っているので詳しい設定方法は割愛します。
RLSを有効にすることで、特定のユーザーのみに閲覧や編集を制限することができますが、今回は自分1人で作業するため無効にしています。
(デフォルトで有効になってて気づくのにめっちゃ時間かかった;;)
ただ、Webアプリを一般公開する際はポリシーを登録してRLSを有効にしましょう。
DBは以下のようにしました。
IDはuuid、作成日時は現在時刻が自動的に入るようにしています。
実装コード
DBは簡単に作成できるため、さっそくコードを書いていきます。
作業環境としては以下の通りになります。
作業環境
root/
├ components/
│ └ TodoItem.tsx
├ pages/
│ └ index.tsx
└ lib/
│ ├ supabase.ts // 初期化
│ └ supabaseFunc.ts // 追加・削除・更新等の関数
実装例
まずは、パッケージをインストールします
// npm
npm install @supabase/supabase-js
// yarn
yarn add @supabase/supabase-js
環境変数も設定します。
NEXT_PUBLIC_SUPABASE_URL=https://*****************.supabase.co
NEXT_PUBLIC_SUPABASE_API_KEY=*****************
詳細は公式ドキュメントをご確認ください。
次にsupabase.ts
で初期化をします。
supabase.ts
import { createClient } from "@supabase/supabase-js";
export type Database = {
id?: number;
text: string;
createdAt?: string;
isDone: boolean;
};
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_API_KEY || "";
const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
export default supabase;
先ほど、DBで設計した4つの要素を取得します。
最初の基本的な環境構築などはShumpeiさんのこちらの記事が参考になりました!多謝!!
次に、supabaseFunc.ts
で必要な関数をまとめます。
今回は、マウント時にDBの全データを取得し、ステートで管理して表示させます。
変更があった場合は、ステートで差分のみ変更するようにしています。
(毎度、DBから全データを取得すると通信量が多くなってしまうので)
必要な関数としては、以下の4つです。
・DBから全データを取得する関数(マウント時)
・新しくToDoを追加する関数
・ToDoを削除する関数
・ToDoのチェックを書き換える関数
supabaseFunc.ts
import supabase from "./supabase";
import { Database } from "./supabase";
export const TABLE_NAME = "sample";
// データの取得
export const fetchDatabase = async () => {
try {
const { data, error } = await supabase.from(TABLE_NAME).select("*").order("createdAt"); // 全てのテーブルデータを日付順でソート
if (error) throw new Error(error.message);
return data;
} catch (error) {
console.error(error);
}
};
// データの追加
export const addSupabaseData = async ({ id, text, isDone }: Database) => {
try {
await supabase.from(TABLE_NAME).insert({ id, text, isDone }); // 新しく行を追加
} catch (error) {
console.error(error);
}
};
// データの削除
export const removeSupabaseData = async (taskId: number) => {
try {
await supabase.from(TABLE_NAME).delete().match({ id: taskId }); // 指定したIDの行を削除
} catch (error) {
console.error(error);
}
};
// アップデート(チェックボックス)
export const updateSupabaseData = async (taskId: number, newStatus: boolean) => {
try {
await supabase.from(TABLE_NAME).update({ isDone: newStatus }).match({ id: taskId }); // 指定したIDのチェック状態を更新
} catch (error) {
console.error(error);
}
};
1つずつ詳細な解説はしませんが、基本的にはテーブル名に、update()
や、insert()
などで変更・追加することができます。match()
で、指定したIDの行を選択することが可能です。
次に、index.tsx
です。
初回のデータ取得に必要なもののみ記述しています。
index.tsx
import supabase, { Database } from "@/lib/supabase";
import { fetchDatabase } from "@/lib/supabaseFunc";
import { useEffect, useState } from "react";
export default function Index() {
const [todoText, setTodoText] = useState<Database[]>([]); // ToDoリストの一覧
useEffect(() => {
(async () => {
const todo = await fetchDatabase();
setTodoText(todo as Database[]); // '{ [x: string]: any; }[] | null'
})();
}, []);
return (・・・)
}
これでステートに必要なデータが入りました。
DBも簡単にいじることができ、データの取得もとても簡単でした!.....しかしこれだけでは不十分です。
理由は、ローカルのステート(表示側)も変更する必要があるからです。
今は、マウント時に初回のデータがステートに入っているだけですので、更新や追加、削除もブラウザ側で適用しなければなりません。
そこで、Supabaseが提供しているリアルタイムリスナーを使います!
コードは以下になります。
// リアルタイムデータ更新
const fetchRealtimeData = () => {
try {
supabase
.channel("table_postgres_changes") // 任意のチャンネル名
.on(
"postgres_changes", // ここは固定
{
event: "*", // "INSERT" | "DELETE" | "UPDATE" 条件指定が可能
schema: "public",
table: TABLE_NAME, // DBのテーブル名
},
(payload) => {
// データ登録
if (payload.eventType === "INSERT") {
const { createdAt, id, isDone, text } = payload.new;
setTodoText((todoText) => [...todoText, { createdAt, id, isDone, text }]);
}
// データ削除
if (payload.eventType === "DELETE") {
setTodoText((todoText) => todoText.filter((todo) => todo.id !== payload.old.id));
}
}
)
.subscribe();
// リスナーの解除
return () => {
supabase.channel("table_postgres_changes").unsubscribe();
};
} catch (error) {
console.error(error);
}
};
追加・削除・更新が起きると関数が実行されます。
コールバック関数から、イベントタイプごとに処理を分けています。
シンプルに追加であれば、ステートに追加、削除はステートから削除です。
ただ、チェックボックスの更新については、ブラウザでチェックの切替変更→DB反映するので更新の"UPDATE"は省略しています。
イメージしやすいように、追加("INSERT")した場合のログを表示させます。
newに新しく追加したものが入ります。
削除の場合は、oldにデータが入ります。
これらをステート側にも反映すればOKです。
残りの、index.tsx
とTodoItem.tsx
も一気に作成します。
index.tsx
import TodoItem from "@/components/TodoItem";
import supabase, { Database } from "@/lib/supabase";
import { fetchDatabase, removeSupabaseData, addSupabaseData, TABLE_NAME, updateSupabaseData } from "@/lib/supabaseFunc";
import { useEffect, useState } from "react";
export default function Index() {
const [inputText, setInputText] = useState(""); // 入力テキスト
const [todoText, setTodoText] = useState<Database[]>([]); // ToDoリストの一覧
// 入力テキスト
const onChangeInputText = (event: React.ChangeEvent<HTMLInputElement>) => setInputText(() => event.target.value);
// ToDoの追加
const onSubmitAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (inputText === "") return;
addSupabaseData({ text: inputText, isDone: false }); // DBに追加
setInputText(() => "");
};
// ToDoの削除
const onClickRemoveTodo = (id: number) => {
const isPublished = window.confirm("本当に削除してもいいですか?この操作は取り消せません。");
if (!isPublished) return;
removeSupabaseData(id);
};
// ToDoのチェック
const onClickTodoCheck = (id: number, isDone: boolean) => {
updateSupabaseData(id, isDone);
};
// リアルタイムデータ更新
const fetchRealtimeData = () => {
try {
supabase
.channel("table_postgres_changes") // 任意のチャンネル名
.on(
"postgres_changes", // ここは固定
{
event: "*", // "INSERT" | "DELETE" | "UPDATE" 条件指定が可能
schema: "public",
table: TABLE_NAME, // DBのテーブル名
},
(payload) => {
// データ登録
if (payload.eventType === "INSERT") {
const { createdAt, id, isDone, text } = payload.new;
setTodoText((todoText) => [...todoText, { createdAt, id, isDone, text }]);
}
// データ削除
if (payload.eventType === "DELETE") {
setTodoText((todoText) => todoText.filter((todo) => todo.id !== payload.old.id));
}
}
)
.subscribe();
// リスナーの解除
return () => {
supabase.channel("table_postgres_changes").unsubscribe();
};
} catch (error) {
console.error(error);
}
};
useEffect(() => {
(async () => {
const todo = await fetchDatabase();
setTodoText(todo as Database[]); // '{ [x: string]: any; }[] | null'
})();
fetchRealtimeData();
}, []);
return (
<>
{/* 追加エリア */}
<form onSubmit={onSubmitAddTodo}>
<input type="text" name="todo" value={inputText} onChange={onChangeInputText} />
<button type="submit" disabled={inputText === ""}>
追加
</button>
</form>
{/* 表示エリア */}
{todoText ? (
<ul>
{todoText.map((item: Database) => (
<TodoItem key={item.id} onClickRemoveTodo={onClickRemoveTodo} onClickTodoCheck={onClickTodoCheck} item={item} />
))}
</ul>
) : (
<p>ToDoリストを追加してください📝</p>
)}
</>
);
}
先ほど作成した、追加・更新・削除の関数も呼べるようにしました。
ToDoの各アイテムは、TodoItem.tsx
に分けて記述しました。
TodoItem.tsx
import { Database } from "@/lib/supabase";
import { useState } from "react";
type Props = {
item: Database;
onClickRemoveTodo: (id: number) => void;
onClickTodoCheck: (id: number, isDone: boolean) => void;
};
const TodoItem = ({ item, onClickRemoveTodo, onClickTodoCheck }: Props) => {
const [isChecked, setIsChecked] = useState(item.isDone); // チェックボックスの状態
return (
<li>
<input
type="checkbox"
checked={isChecked}
onChange={() => {
onClickTodoCheck(item.id!, !isChecked);
setIsChecked(() => !isChecked);
}}
/>
{item.text}
<button onClick={() => onClickRemoveTodo(item.id!)}>削除</button>
</li>
);
};
export default TodoItem;
先述したように、チェックボックスはローカル→DBで反映させています。
動作デモ
スタイルを当てていないのですが、以下のような動作になります。
リロードして問題なければOKです。
ちなみに、Supabaseの管理画面では以下のように確認できます。
おわり
いかがだったでしょうか。
リアルタイムリスナーでデータの更新などする方法を解説しました。
Cloud Firestoreでもリアルタイム機能はありますが、徐々にRDBに慣れたい方などは第一歩としてもSupabseいいと思います。
勢いがあるサービスなので、今後も個人開発で使っていきたいと思います。
どんどん開発・改善が進んでいるサービスなので、今後実案件で使えることを楽しみにしています🙌
参考
この記事へのコメント
この記事にはまだコメントがありません。