Supabase Realtime を使ってみる

2021.10.16

💻
TECH

Supabase Realtime を使ってみる


[2022/08/15 更新]

この記事を更新してから Supabase も進化しました。

こちらで新たに VsCode の Remote Containers 拡張機能を使った開発環境の立ち上げ方法を紹介しています。

開発環境の構築方法については、そちらの記事の方が最新の情報に基づいていますので併せてご覧ください。


前回は Supabase Database を利用して、Slack のクローンアプリを作成していきました。

前回の記事

ただ、前回までの状態では最新のメッセージを受信するのに、毎回リロードしなければいけませんでした。

今回はこれを解決するために、Supabase の Realtime の機能を利用してみます。

今回のソースコードはこちらです。


Supabase Realtime

名前の通り Realtime にデータベースの変更を受け取るための仕組みです。

PostgreSQL のレプリケーションの機能を使って実現されているそうです。

この機能を使う際の注意点として、現状の Realtime の機能では PostgreSQL の Row Level Security が効かないようですので、フロントエンドから使う場合にはログインユーザーのプライベートなデータなどで使用するのは避けた方が良いです。

Realtime のセキュリティ向上に取り組んでいるとの、公式ページの言及もあるので本格的な利用はそれを待ってからでしょうね。

あるいは、プライベートなデータを全く扱わないのであれば、簡便に変更を受け取ることができるので使い所はあるかもしれません。

ちなみに Realtime の機能を無効化する方法はこちらに記載があります。


追記(2021/12/06)

12/1 に PostgreSQL の RLS が Realtime で利用可能になりました!

詳細は公式ブログをご覧ください。


使い方

では使い方を見ていきます(公式の記載はこちら)

Supabase の Realtime の機能でデータベースのテーブルの変更受信するには以下のように記述します。

const messageListener = supabase
  .from("messages")
  .on("INSERT", (payload) => handleNewMessage(payload.new))
  .on("DELETE", (payload) => handleDeletedMessage(payload.old))
  .subscribe();

from までは今までデータを取得する際に使用していた時の書き方と同じです(前回の記事で紹介しています)。

単純にデータを取得する際には、 from の後に select を呼んでいましたが、Realtime では後ろに on を使って変更を受け取る操作の指定をします。

on の第一引数に'INSERT', 'UPDATE', 'DELETE'のいずれかを、第二引数に変更のあったデータを処理する関数を与えます。

そして、その後ろに subscribe を呼びます。

INSERT と UPDATE 2種類の変更を受け取りたい時には、メソッドチェーンの形でそれぞれの on を呼び出せば可能です。

また UPDATEDELETEonに指定した際にデフォルトでは、 payload として変更後のデータ(new)のみが返ってきます。

変更前のデータも受け取りたい場合は、データベースクライアントなど(supabase.io 上で作成したプロジェクトならプロジェクトのデータベースのページ)で以下を実行すると可能になるようです。

alter table "your_table" replica identity full;

もちろん "your_table" は自身のテーブル名に変更してください。

これを行うと上記例のように payload.new payload.old で新旧データにアクセスできます。

最後に、変更の受信をやめたい時にはsubscribe関数の戻り値の unsubscribeメソッドを呼び出します。

messageListener.unsubscribe();

このように非常に簡単に使うことができます。


前回までのアプリに使ってみる

では実際に前回まで作っていた、チャットアプリに利用してみます。

まずはメッセージの受け取りです。

以下のようにしてみます。

hooks/use-data-loader.tsx

export const fetchUserById = async (userId: string) => {
  try {
    let { body } = await supabase
      .from<definitions["users"]>("users")
      .select(`*`)
      .eq("id", userId);
    return body && body.length > 0 ? body[0] : null;
  } catch (error) {
    console.log("error", error);
  }
};

export const useDataLoader = (props: { channelId: string }) => {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchChannels());
  }, [dispatch]);

  const handleNewMessage = useCallback(
    async (newMessage: definitions["messages"]) => {
      const user = await fetchUserById(newMessage.user_id);
      const message: Message = {
        ...newMessage,
        author: user || null,
      };
      dispatch(messagesSlice.actions.messageAddedOrUpdated(message));
    },
    [dispatch]
  );

  useEffect(() => {
    const messageListener = supabase
      .from<definitions["messages"]>("messages")
      .on("INSERT", (payload) => handleNewMessage(payload.new))
      .subscribe();

    return () => {
      messageListener.unsubscribe();
    };
  }, [handleDeletedMessage, handleNewMessage]);

  // ...
  // ...  省略
  // ...
};

それほど解説もいらないかもしれないですが一応。

useEffect の中で messages テーブルの INSERT に Subscribe しています。

また、 useEffect 内の returnunsubscribe を呼び出すことで、

コンポーネントのアンマウント以降は変更を受け取らないようにします。

INSERT の変更を受け取った際には新規データを handleNewMessage で処理しています。

handleNewMessage 内部ではメッセージを受け取るたびに fetchUserByIdでユーザーデータを取得しています。これは、Redux のストア内では author としてユーザーデータを紐付けて保持しているためです。これに合わせてメッセージを受け取るたびにユーザーを紐付けるようにしています。

これでメッセージが追加されるたびに新規メッセージが表示されます。もうリロードは必要ありません!

supabase_realtime_sample

続けて、チャンネルが追加された時にも同期されるようにしましょう。

hooks/use-data-loader.tsx

export const useDataLoader = (props: { channelId: string }) =>

  // 追加!!
  const handleNewChannel = useCallback(
    (channel: definitions['channels']) => {
      dispatch(channelsSlice.actions.channelAddedOrUpdated(channel));
    },
    [dispatch]
  );

  useEffect(() => {
    const messageListener = supabase
      .from<definitions['messages']>('messages')
      .on('INSERT', (payload) => handleNewMessage(payload.new))
      .subscribe();

		// 追加!!
    const channelListener = supabase
      .from<definitions['channels']>('channels')
      .on('INSERT', (payload) => handleNewChannel(payload.new))
      .subscribe();

    return () => {
      messageListener.unsubscribe();
      channelListener.unsubscribe(); // 追加!!
    };
  }, [handleNewChannel, handleNewMessage]);

}

こちらはメッセージのように余計な追加データの取得はないので、変更のあったデータを Redux のストアに追加しているだけです。


まとめ

今回は Supabase の Realtime の機能を試してみました。セキュリティの問題が解消すれば、簡単に使えて良さそうだと思いました。

また非常に簡単に実装できるがゆえ、中で何をやっているのだろうというのは気なります。機会があれば、具体的にどのように Supabase の Realtime の機能が実現されているのか調べてみたいです。

今回のソースコードはこちらにあります。何かの参考になれば幸いです。


Homeへ戻る
profile picture

Kosuke Kihara

I'm a Web Developer👑 who shares tips on Tech, Productivity, Design, and much more!

Kohsuk11KOHSUKkohsuk.tech@gmail.com