💬

Expo × Supabase × React Native でリアルタイムチャットを作ってみた

に公開

はじめに

この作品は、別プロジェクトでリアルタイムチャットの機能が必要になった際に、まず作ってみたサンプルです。
実際にこの構成をもとに、既読処理、メッセージの削除、「〇〇が参加しました」といったアラート、プッシュ通知の実装まで行うことができました。

そのプロジェクトが一区切りついたあと、このサンプルは数ヶ月ほど眠っていたのですが、せっかくなので今回を機に紹介しておこうと思います。


使用技術

  • React Native (Expo + TypeScript)
  • Supabase(Database + Realtime)
  • AsyncStorage(ユーザー名保存)
  • Day.js(時刻整形)

アプリの画面イメージ

ユーザー名入力画面

チャットルーム画面(iOS / Android / Web)

iOS

Android

Web


💾 チャットデータの構成

チャットのメッセージは Supabase 上の messages テーブルに保存されます。
このテーブルは、ユーザー名・本文・送信時刻の3つを基本要素とする、シンプルな構成です。

messages テーブルのSQL定義

CREATE TABLE messages (
  id serial PRIMARY KEY,
  username text NOT NULL,
  content text NOT NULL,
  created_at timestamp with time zone DEFAULT timezone('utc', now())
);

🔍 各カラムの解説

カラム名 説明
id serial メッセージの一意なID(自動採番)
username text 投稿者の名前(ユーザーが任意に入力)
content text メッセージ本文
created_at timestamp with time zone 投稿日時(デフォルトでUTCの現在時刻)

※ Supabase の Realtime を利用するため、このテーブルに対して「Realtimeを有効化」しておく必要があります。


🔸 メッセージオブジェクトの構造

クライアントアプリでは、Supabaseから取得した各メッセージを以下のようなオブジェクトとして扱います。

type Message = {
  id: number; // メッセージの一意なID(自動的に割り当てられる)
  username: string; // 投稿者の名前(任意に入力されたもの)
  content: string; // メッセージ本文(送信されたテキスト)
  created_at: string; // 投稿日時(ISO形式の文字列、例: "2025-06-27T14:02:45.000Z")
};

たとえば、1件のメッセージデータは次のようになります。

{
  "id": 1,
  "username": "テストユーザー",
  "content": "こんにちは!",
  "created_at": "2025-06-27T14:02:45.000Z"
}

これらのデータを時刻順に並べてリスト表示し、チャットUIとして描画しています。


✉️ メッセージの送信

チャット画面の下部にあるテキスト入力欄にメッセージを入力し、送信ボタン(✈️)をタップすると、その内容が Supabase に保存されます。これにより、他のクライアントにもリアルタイムでメッセージが共有される仕組みです。

🔁 送信処理の流れ

  1. 入力欄にメッセージを入力
  2. 「✈️」ボタンを押すと、handleSendMessage 関数が実行
  3. Supabase の messages テーブルに insert() で追加
  4. 入力欄が空になり、画面が自動スクロールされる
// 送信関数の一部抜粋
const handleSendMessage = async () => {
  if (message.trim()) {
    const { error } = await supabase.from("messages").insert([{
      username,
      content: message,
      created_at: new Date().toISOString(),
    }]);

    if (error) {
      console.error("Error sending message:", error);
    }

    setMessage(""); 
    setInputHeight(40); 

    setTimeout(() => {
      flatListRef.current?.scrollToEnd({ animated: true });
    }, 100);
  }
};
  • created_at はクライアントで付与していますが、DB側でもデフォルト値が設定されているためどちらでも対応可能です。
  • .trim() によって空白だけの送信を防止。
  • FlatList を使って最新の位置までスクロール。

📦 過去メッセージの取得(初期ロード)

チャット画面が表示されたとき、まず Supabase の messages テーブルから、既存のメッセージを取得して一覧として表示します。

const loadMessages = async () => {
  try {
    const { data, error } = await supabase
      .from("messages")
      .select("*")
      .order("created_at", { ascending: true });

    if (error) throw error;

    if (data) {
      setMessages(data);
      setTimeout(() => {
        flatListRef.current?.scrollToEnd({ animated: true });
      }, 100);
    }
  } catch (error) {
    console.error("Failed to load messages", error);
  }
};
  • 初回取得時は created_at の昇順で並べ替え、時系列で表示。
  • ロード後に下部スクロールで最新メッセージがすぐ見える。

📡 リアルタイム購読(新着メッセージの受信)

Supabase の Realtime を使うことで、他のユーザーが送信したメッセージを自動的に受信して表示できます。
これは insert() によってメッセージが送信されるたびに発火する仕組みで、ユーザーに更新を意識させることなくチャットが成立します。

useEffect(() => {
  loadMessages();

  const subscription = supabase
    .from("messages")
    .on("INSERT", (payload) => {
      const newMessage = payload.new as Message;
      setMessages((prev) => [...prev, newMessage]);
    })
    .subscribe();

  return () => {
    supabase.removeSubscription(subscription);
  };
}, []);
  • .on("INSERT") によって、他ユーザーの insert() を監視。
  • payload.new に含まれるメッセージを配列に追加。
  • subscribe() 開始と removeSubscription() で適切に管理。

プロジェクトの実行

Githubからプロジェクトのクローン

git clone https://github.com/Gratien583/tsx-Supabase-ChatApp-Beta.git
cd tsx-Supabase-ChatApp-Beta

🔧 Supabase 設定

このアプリを動作させるには、.env ファイルに Supabase プロジェクトの URL と API キーを設定してください。

1. Supabase プロジェクトの作成とキーの取得

  • https://app.supabase.com/ にアクセスし、新規プロジェクトを作成します。
  • 「Project URL」と「anon key(APIキー)」を取得します。

2. .env ファイルの作成

プロジェクトルートに .env ファイルを作成し、以下のように記述します:

EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_KEY=your-anon-key

.env.gitignore に含め、公開しないよう注意してください。

3. supabaseClient.ts の内容

import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_KEY;

if (!supabaseUrl || !supabaseKey) {
  throw new Error("Supabase環境変数が設定されていません。");
}

export const supabase = createClient(supabaseUrl, supabaseKey);

依存関係のインストール

npm install

Expoにて実行

npx expo start

終わりに

本記事のチャットアプリは、あるチーム開発の下準備として急ごしらえで作ったものです。
ですが、個人的に意外と使い勝手がよく、プロジェクトの土台としてかなり活躍してくれました。

技術的にすごい仕組みは使っていませんが、「まず動くものを作る → 機能を増やす」サイクルを体感できた良い経験でした。

Discussion