【React】アプリをDBにつないでみた

# はじめに
以前作成した学習記録アプリを改良し、SupabaseというBaaSと接続して、データを永続化するように実装しました。
今回はその時の手順をまとめたいと思います。

↓前回の記事はこちらです
https://qiita.com/Daisuke_Kikukawa/items/f404a53f4e55b2a8e033

# 制作したアプリ
機能面は変わらないのですが、今回新たにDBに接続しました。
最初は、ローカルでデータを管理していた学習記録アプリを、Supabaseと連携させることで、データを永続的に保存できるようになりました。
これにより、リロードしたりブラウザを閉じても、次回アクセス時にデータが保持されるようになりました。

# 実装手順

## 1.Supabaseのセットアップ
Supabaseの公式ウェブサイト（https://supabase.com） にアクセスし、ダッシュボードから「New Project」をクリックします。
プロジェクトのダッシュボードで「Settings」→「API」を選択し、**Project URL**と**anon public**キーをコピーします。
これらがそれぞれのちに設定する環境変数の`REACT_APP_SUPABASE_URL`と`REACT_APP_SUPABASE_ANON_KEY`になります。
次にReactプロジェクトに環境変数の追加を行います。
ルートディレクトリに.envファイルを作成し、以下の行を追加します。

```javascript
REACT_APP_SUPABASE_URL=プロジェクトURL
REACT_APP_SUPABASE_ANON_KEY=anon publicキー
```

Supabaseクライアントライブラリのインストールを行うため、ターミナルで以下のコマンドを実行します。
```bash
npm install @supabase/supabase-js
```
これらの手順を行うことでReactアプリケーションからSupabaseを使用する準備が整います。
これらの設定を使用してSupabaseクライアントを初期化し、データベース操作を実装していきます。

## 2. Supabaseクライアントの初期化
プロジェクトのsrcディレクトリにsupabase.jsという名前の新しいファイルを作成し、supabase.jsファイルの先頭に以下の行を追加します。
この記述を加えることでSupabaseクライアントを作成するための関数をインポートすることができます。
```javascript
import { createClient } from '@supabase/supabase-js'
```
次に.envファイルに設定した環境変数を取得します：
```javascript
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
```
最終的に、`supabase.js`ファイルは以下のようになります。
```javascript
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)
```

## 3. データベース操作関数の実装
`supabaseFunctions.js`ファイルを作成し、データベース操作のための関数を実装します
データベースを操作するための関数は、アプリケーションとSupabaseデータベース間の橋渡しをする重要な部分になります。
今回は、主要CRUDのうち全件取得、新規作成、削除の実装を行いました。
ファイルの先頭に以下の行を追加して、初期化済みのSupabaseクライアントをインポートします。
```javascript
import { supabase } from "./supabase.js";
```
**getAllRecords関数の実装**
この関数でデータベースから全ての学習記録を取得できるようになります。
```javascript
export const getAllRecords = async () => {
  const records = await supabase.from("study-record").select("*");
  return records.data;
};
```
`supabase.from("study-record")`で対象テーブルを指定し、`.select("*")`ですべてのカラムを選択しています。

**addTodo関数の実装**
この関数で新しい学習記録をデータベースに追加ができるようになります。
```javascript
export const addTodo = async (title, time) => {
  await supabase.from("study-record").insert({ title: title, time: time });
};
```
`supabase.from("study-record")`で対象テーブルを指定し、`supabase.from("study-record").insert()`で新しいレコードを挿入します。

**deleteTodo関数の実装**
この関数で指定されたIDの学習記録を削除することができます。
```javascript
export const deleteTodo = async (id) => {
  (await supabase.from("study-record").delete().eq("id", id));
}
```
supabase.from("study-record").delete()で削除操作を指定し、.eq("id", id)で削除対象のレコードを指定しています。

最終的に、`supabaseFunctions.js`ファイルは以下のようになります

```javascript
mport { supabase } from "./supabase.js";

export const getAllRecords = async () => {
  // 実装は上記のとおり
};

export const addTodo = async (title, time) => {
  // 実装は上記のとおり
};

export const deleteTodo = async (id) => {
  // 実装は上記のとおり
};
```
データベース操作用の関数を実装することにより、Reactコンポーネントから簡単にデータベース操作を行うことができます。各関数は非同期で実装されており、async/awaitを使用してPromiseベースの操作を行っています。

## 4. Reactコンポーネントの修正
次に既存のReactコンポーネント（App.js）を修正して、Supabaseを使用するように変更します。
`App.js`の先頭に以下の行を追加します
```javascript
import { useState, useEffect } from "react";
import { getAllRecords, addTodo, deleteTodo } from "../utils/supabaseFunctions";
```
`getAllRecords`,`addTodo`,`deleteTodo`は、先ほど実装したデータベース操作関数になります。
コンポーネント内で以下の状態変数を設定します
```javascript
const [isLoading, setIsLoading] = useState(true);
const [records, setRecords] = useState([]);
const [newTitle, setNewTitle] = useState("");
const [newTime, setNewTime] = useState("");
const [error, setError] = useState("");
const [totalTime, setTotalTime] = useState(0);
```
- `isLoading`: データ取得中かどうかを示すフラグ
- `records`: 取得した学習記録のリスト
- `newTitle`, newTime: 新規入力用の状態
- `error`: エラーメッセージ保存用
- `totalTime`: 合計学習時間

**useEffectを使用したデータの取得**
コンポーネントのマウント時にデータを取得するためにuseEffectを使用します
```javascript
useEffect(() => {
  getRecords();
}, []);

const getRecords = async () => {
  try {
    const fetchedRecords = await getAllRecords();
    setRecords(fetchedRecords);
    setIsLoading(false);
  } catch (error) {
    console.error("Error fetching records:", error);
    setError("データの取得に失敗しました。");
    setIsLoading(false);
  }
};
```

**新規追加処理の実装**
```javascript
const handleSubmit = async (e) => {
  e.preventDefault();
  if (newTitle.trim() === "" || newTime.trim() === "") {
    setError("学習内容と学習時間を入力してください");
    return;
  }
  try {
    const addedRecord = await addTodo(newTitle, newTime);
    if (addedRecord) {
      setRecords([addedRecord, ...records]);
      setNewTitle("");
      setNewTime("");
      setError("");
    }
  } catch (error) {
    console.error("Error adding record:", error);
    setError("記録の追加に失敗しました。");
  }
};
```

**削除機能の実装**
```javascript
const handleDelete = async (id) => {
  try {
    const success = await deleteTodo(id);
    if (success) {
      setRecords(records.filter(record => record.id !== id));
    }
  } catch (error) {
    console.error("Error deleting record:", error);
    setError("記録の削除に失敗しました。");
  }
};
```

**HTMLの実装**
以下が取得したデータを表示し、新規追加と削除機能のHTML部分になります
```javascript
return (
  <>
    {isLoading ? (
      <h1>Loading...</h1>
    ) : (
      <div className="container">
        <h1>学習記録アプリ</h1>
        {error && <p className="error">{error}</p>}
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            value={newTitle}
            onChange={(e) => setNewTitle(e.target.value)}
            placeholder="学習内容"
          />
          <input
            type="number"
            value={newTime}
            onChange={(e) => setNewTime(e.target.value)}
            placeholder="学習時間（時間）"
          />
          <button type="submit">追加</button>
        </form>
        <ul>
          {records.map((record) => (
            <li key={record.id}>
              {record.title} - {record.time}時間
              <button onClick={() => handleDelete(record.id)}>削除</button>
            </li>
          ))}
        </ul>
        <p>合計学習時間: {totalTime}時間</p>
      </div>
    )}
  </>
);
```
- ローディング中は「Loading...」を表示します。
- エラーメッセージがある場合は表示します。
- フォームで新規記録の入力を受け付けます。
- 記録のリストを表示し、各記録に削除ボタンを付けます。
- 合計学習時間を表示します。

以上が実装部分になります。

# 大変だったところ
Reactアプリケーションをローカルストレージからデータベースに移行する部分が、想像していたよりも難しかったです。
特に非同期処理の管理が難しかったです。ローカルストレージを使用していた時は、すべて同期的に行えていたのですが、Supabaseを利用することで、データベース操作が非同期になりました。そのため、useEffectを使って実装を行う部分で少し詰まってしまいました。
また、エラーハンドリングの扱いにも苦労しました。ローカルでの保存時では考慮する必要のなかったネットワークエラーやデータベースエラーなど、考えないといけないエラーパターンが多かったです。今後は各データベース操作関数にtry-catch構文を導入し、発生したエラーを適切にキャッチして処理する習慣をつけようと思います。

# 最後に
DB接続のやり方がわかったことで自分の実装の幅がかなり広がったと感じています！
まだまだ基本的な部分しかわかっていないので、より深めていきたいと思います！

# JISOUのメンバー募集中
プログラミングコーチングJISOUではメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか？

気になる方はぜひHPからライン登録お願いします👇

https://projisou.jp