react+Supabase

Reactのチュートリアル記事です。
名刺アプリ Supabase連携 (後編)です。
本記事は、以下の記事をモチーフに作成した名刺アプリのチュートリアルです。

こちらの「4. 名刺アプリの開発」をベースに進めてます。元記事では、データの格納にBaaS(Backend as a Service)であるSupabaseを利用したものとなってますので、同様にSupabaseを活用して開発したいと思います。なお、参考記事ではCI/CDの為のテスト実装まで触れてますが、本記事ではテストまでは言及していません。機能面に焦点を当てています。
前編ではSupabaseの設定、環境構築、SupabaseのDBからテーブル情報の取得までを行いました。
後編の今回は、新規データ登録、データ編集、データ削除機能の実装と、GitHub、Firebaseの連携について記載します。

なお、前編記事はこちらです。

はじめに

本記事は、Reactの初学者向けのチュートリアルコンテンツです。BaaSである、Supabaseのデータ処理を実装し、実際にアプリケーションをデプロイしてユーザーが使えるリリースまでを行えます。
React環境は、Vite+TypeScript
CSS+UIツールとしては、Chakra UI、アイコンとして、React Icons
BaaSとしてSupabase
ホスティングサービスは、GoogleのFirebaseを利用しています。
本記事で、以下の対応が可能です。

  • React+Vite環境の構築
  • useStateによるState管理、Propsの扱い、useEffectの使用
  • Formイベント、イベントハンドラーの扱い
  • React Routerによるルート設定、制御
  • TypeScriptのコード記述
  • ChakraUIの導入・利用
  • Reactアイコンの利用
  • async/awaitによる非同期処理
  • Supabaseの環境設定とテーブル処理(表示、登録、更新、削除)
  • GitHubリポジトリの扱い
  • Firebaseホスティングの利用、GitHubと連携した自動デプロイ 等

作成するアプリの構造は下図のイメージとなります。

  • App.tsx
    トップコンポーネントです。
  • components/Home.tsx
    名刺を表示するためのID入力及び新規登録ボタンを配置したHome画面です。初期画面となります。ルーティングは”/”で設定します。
  • components/Cards.tsx
    ID入力後、入力されたIDを元に名刺情報をsupabaseより読み込み表示します。ルーティングは”/card/id名”でセットします。
  • components/Edit.tsx
    名刺情報を編集・更新するコンポーネントです。Chakura UIのモーダルを利用します。
  • components/Delete.tsx
    名刺情報を削除するコンポーネントです。Chakura UIのモーダルを利用します。
  • components/Register.tsx
    新規にID、名刺情報を登録するコンポーネントです。ルーティングは、”/register”でセットします。

5. 新規登録機能

後編と言うことで、前編の続き、5章から開始です。
ここでは、名刺データ新規登録のコンポーネントを作成していきます。componentsフォルダにRegister.tsxを作成します。
中身のコードは後ほど、実装していきます。

5.1 App.tsxの修正

Registerコンポーネントの実装に当たり、まず、App.tsxを変更していきます。
まずは、インポート箇所とstateの定義です。

// /src/App.tsx

import { useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom"
import { useToast } from "@chakra-ui/react";
import Home from "./components/Home"
import Cards from "./components/Cards";
import { CardData } from "./cardData";
import { supabase } from "./supabaseClient";
import Register from "./components/Register";//Registerのインポート追加

function App() {
  const [userid, setUserid] = useState<string>();
  const [cardData, setCardData] = useState<CardData[]>([]);
  const [newCard, setNewCard] = useState<CardData>({//Register用に、個別のstate、newCard定義
    user_id: '',
    name: '',
    description: '',
    github_id: '',
    qiita_id: '',
    x_id: '',
    skill_id: 0,
    skills: '',
  });
  const [loading, setLoading] = useState<boolean>(false);
  const toast = useToast()
  const navigate = useNavigate()

インポートについては、新しいコンポーネント、Registerをインポートしています。
stateはRegister用のもの、newCard, setNewCard をセットしています。cardDataを流用する形でもいいかも知れませんが、cardDataが配列型なのに対し、1組のデータセットであるRegister向けにはオブジェクト型のnewCardを設ける事にしました。

続いてDB処理の箇所です。

 // /src/App.tsx
 
 //DBへの新規データ登録
  const insertDb = async (card: CardData): Promise<boolean> => {//仮設置
    return true;
  }
  //登録ユーザの重複チェック
  const userCheckDb = async (card: CardData): Promise<boolean> => {//仮設置
    return true;
  }

DBテーブルに新規にデータ登録する関数として、insertDbをセットしています。
また、登録時にユーザの重複チェックを行う関数として、userCheckDbをセットしています。共に今時点は具体的な処理は記載してません。trueを返すのみです。
後ほど、具体的な処理内容は記載します。

最後にJSXの箇所です。

 // /src/App.tsx
 
   return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} />} />
        {/*ルート追加 ここから */}
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
        {/*ルート追加 ここまで */}
      </Routes>
    </>
  )

新しく、Registerコンポーネント用に、ルートを設定しています。パスは、’/card/register’ です。
Registerコンポーネントに渡すPropsとして、loading, newCard, setNewCard, setUserid, insertDb, userCheckDb を設定しています。

現時点のApp.tsxのコード全文は以下の通りです。

// /src/App.tsx

import { useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom"
import { useToast } from "@chakra-ui/react";
import Home from "./components/Home"
import Cards from "./components/Cards";
import { CardData } from "./cardData";
import { supabase } from "./supabaseClient";
import Register from "./components/Register";//Registerのインポート追加

function App() {
  const [userid, setUserid] = useState<string>();
  const [cardData, setCardData] = useState<CardData[]>([]);
  const [newCard, setNewCard] = useState<CardData>({//Register用に、個別のstate、newCard定義
    user_id: '',
    name: '',
    description: '',
    github_id: '',
    qiita_id: '',
    x_id: '',
    skill_id: 0,
    skills: '',
  });
  const [loading, setLoading] = useState<boolean>(false);
  const toast = useToast()
  const navigate = useNavigate()

  // Supabaseからデータを取得する関数
  const selectDb = async (): Promise<boolean> => {
    setLoading(true);

    // Step 1: usersテーブルからuser_idを取得
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', userid);

    if (userError || !userData || userData.length === 0) {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user data:', userError);
      setLoading(false);
      return false;
    }
    console.log('step1', userData)

    // Step 2: user_skillテーブルから該当するskill_idを取得
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .select('skill_id')
      .eq('user_id', userid);

    if (userSkillError || !userSkillData || userSkillData.length === 0) {
      toast({
        title: 'Skill IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user skills:', userSkillError);
      setLoading(false);
      return false;
    }
    console.log('step2', userSkillData)
    const skill_ids = userSkillData.map((skill) => skill.skill_id);

    // Step 3: skillsテーブルからnameを取得
    const { data: skillsData, error: skillsError } = await supabase
      .from('skills')
      .select('name')
      .eq('id', skill_ids);

    if (skillsError || !skillsData || skillsData.length === 0) {
      toast({
        title: 'skillsが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching skills:', skillsError);
      setLoading(false);
      return false;
    }

    console.log('step3', skillsData)
    const skillsString = skillsData.map(skill => skill.name);

    // Step4:結果をcombinedDataに集約
    const combinedData = userData.map((user) => ({
      ...user, skill_id: skill_ids, skills: skillsString,
    }));

    console.log('step4', combinedData)
    setCardData(combinedData);
    toast({
      title: 'データを取得しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    setLoading(false);
    return true;  // データ取得に成功した場合はtrueを返す
  };

  //DBへの新規データ登録
  const insertDb = async (card: CardData): Promise<boolean> => {//仮設置
    return true;
  }
  //登録ユーザの重複チェック
  const userCheckDb = async (card: CardData): Promise<boolean> => {//仮設置
    return true;
  }

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUserid(e.target.value)
  }

  const handleSearch = async () => {
    if (userid) {
      const success = await selectDb();
      if (success) {
        navigate(`/card/${userid}`);
      }
    } else {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
    }
  };

  return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} />} />
        {/*ルート追加 ここから */}
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
        {/*ルート追加 ここまで */}
      </Routes>
    </>
  )
}

export default App

5.2 Home.tsxの修正

続いて、Home.tsxを修正します。「新規登録」ボタンクリック時に、Registerコンポーネントに遷移するようにします。
以下は、Home.tsxコード全体です。

// /src/components/Home.tsx
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";//React Router, useNavigateインポート

type HomeProps = {
    userid: string;
    loading: boolean;
    handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
    handleSearch: () => Promise<void>;
}

const Home: React.FC<HomeProps> = ({ userid, loading, handleInputChange, handleSearch }) => {
    const navigate = useNavigate();//useNavigate追加
    return (
        <>
            <Flex alignItems='center' justify='center' p={5}>
                <Card>
                    <CardHeader>
                        <Heading size='md' textAlign='center'>Name Card App</Heading>
                    </CardHeader>
                    <CardBody>
                        ID
                        <Input
                            autoFocus
                            placeholder='IDを入力'
                            name='id'
                            value={userid}
                            onChange={handleInputChange}
                        />
                        <Box mt={4} >
                            <Button
                                colorScheme='blue'
                                onClick={handleSearch}
                                w='100%'
                                loadingText='Loading'
                                isLoading={loading}
                                spinnerPlacement='start'
                            >名刺を表示</Button>

                        </Box>
                        <Box mt={4} >
                            <Button
                                colorScheme='blue'
                                variant='outline'
                                w='100%'
                                onClick={() => navigate('/card/register')}//追加、クリックでRegister画面に遷移
                            >
                                新規登録</Button>
                        </Box>
                    </CardBody>
                </Card>
            </Flex>
        </>
    )
}
export default Home;

Registerへの遷移の為、React RouterのuseNavigateを新たにインポートしています。更に、const navigate = useNavigate()でnavigate機能の定義。
そして、JSX箇所の「新規登録」ボタンに、onClick={() => navigate(‘/card/register’)} を追加しています。クリックすれば、/card/register に遷移すると言う内容です。

5.3 Register.tsxの作成

それでは、Register.tsxにコードを記載していきます。
以下は、Register.tsxのコード全文です。

// /src/components/Register.tsx
import { useNavigate } from "react-router-dom";//React Router, useNavigateインポート
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input, Select, Text, Textarea, useToast } from "@chakra-ui/react";//Chakra UIインポート
import { CardData } from "../cardData";//CardData型定義のインポート

type RegisterProps = {//Propsの型定義
    loading: boolean;
    newCard: CardData;
    setNewCard: React.Dispatch<React.SetStateAction<CardData>>
    setUserid: React.Dispatch<React.SetStateAction<string | undefined>>
    insertDb: (card: CardData) => Promise<boolean>
    userCheckDb: (card: CardData) => Promise<boolean>
};

const Register: React.FC<RegisterProps> = ({ loading, newCard, setNewCard, setUserid, insertDb, userCheckDb }) => {
    const toast = useToast()
    const navigate = useNavigate();

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setNewCard({
            ...newCard,
            [name]: value,
        });
    };

    const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        const { name, value } = e.target;
        setNewCard({
            ...newCard,
            [name]: value,
        });
    };

    const handleRegister = async () => {
        if (newCard.user_id && newCard.name && newCard.description && newCard.skill_id) {
            const isUserIdUnique = await userCheckDb(newCard);
            if (isUserIdUnique) {
                await insertDb(newCard);
                setUserid(newCard.user_id);
                setNewCard({
                    user_id: '',
                    name: '',
                    description: '',
                    github_id: '',
                    qiita_id: '',
                    x_id: '',
                    skill_id: 0,
                    skills: '',
                });
                navigate('/');
            }
        } else {
            toast({
                title: '*の必須項目を入力してください',
                position: 'top',
                status: 'error',
                duration: 2000,
                isClosable: true,
            })
        }
    };

    return (
        <>
            <Flex alignItems='center' justify='center' p={5}>
                <Card maxW='400px'>
                    <CardHeader>
                        <Heading size='md' textAlign='center'>カード登録</Heading>
                    </CardHeader>
                    <CardBody>
                        <Text>ID名*</Text>
                        <Input
                            autoFocus
                            placeholder='ご希望のID名を入力'
                            name='user_id'
                            value={newCard.user_id}
                            onChange={handleInputChange}
                        />
                        <Text mt='3'>お名前*</Text>
                        <Input
                            placeholder='お名前を入力'
                            name='name'
                            value={newCard.name}
                            onChange={handleInputChange}
                        />
                        <Text mt='3'>自己紹介*</Text>
                        <Textarea
                            placeholder='<h1>HTMLタグも入力できます</h1>'
                            name='description'
                            value={newCard.description}
                            onChange={handleTextareaChange}
                        />
                        <Text mt='3'>好きな技術*</Text>
                        <Select
                            placeholder='選択してください'
                            name='skill_id'
                            value={newCard.skill_id.toString()} // 数値を文字列に変換して表示
                            onChange={(e) => setNewCard({ ...newCard, skill_id: Number(e.target.value) })}
                        >
                            <option value='1'>React</option>
                            <option value='2'>TypeScript</option>
                            <option value='3'>GitHub</option>
                        </Select>
                        <Text mt='3'>GitHub ID</Text>
                        <Input
                            name='github_id'
                            value={newCard.github_id}
                            onChange={handleInputChange}
                        />
                        <Text mt='3'>Qiita ID</Text>
                        <Input
                            name='qiita_id'
                            value={newCard.qiita_id}
                            onChange={handleInputChange}
                        />
                        <Text mt='3'>X(Twitter) ID</Text>
                        <Input
                            name='x_id'
                            value={newCard.x_id}
                            onChange={handleInputChange}
                        />
                        <Box mt={4} textAlign='center'>
                            <Button
                                isLoading={loading}
                                loadingText='Loading'
                                colorScheme='blue'
                                spinnerPlacement='start'
                                mr='5'
                                onClick={handleRegister}
                            >登録</Button>
                            <Button
                                colorScheme='gray'
                                variant='outline'
                                onClick={() => {
                                    navigate('/');
                                }
                                }>戻る</Button>
                        </Box>
                    </CardBody>
                </Card>
            </Flex >
        </>
    )
}
export default Register;

それでは、Register.tsxについて解説していきます。
まず冒頭のインポート、コンポーネント定義の箇所です。

// /src/components/Register.tsx
import { useNavigate } from "react-router-dom";//React Router, useNavigateインポート
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input, Select, Text, Textarea, useToast } from "@chakra-ui/react";//Chakra UIインポート
import { CardData } from "../cardData";//CardData型定義のインポート

type RegisterProps = {//Propsの型定義
    loading: boolean;
    newCard: CardData;
    setNewCard: React.Dispatch<React.SetStateAction<CardData>>
    setUserid: React.Dispatch<React.SetStateAction<string | undefined>>
    insertDb: (card: CardData) => Promise<boolean>
    userCheckDb: (card: CardData) => Promise<boolean>
};

const Register: React.FC<RegisterProps> = ({ loading, newCard, setNewCard, setUserid, insertDb, userCheckDb }) => {
    const toast = useToast()
    const navigate = useNavigate();

インポートについては、記載の通りです。React Router, Chakra UI, CardDataの型情報のインポートを行っています。

type設定については、App.tsxからloading, newCard, setNewCard, setUserid, insertDb, userCheckDbがPropsとして渡されますので、その定義を行っています。newCardは、CardData(/src/cardData.ts)で定義された型情報を持ったオブジェクトです。
set関数は、前編4.2で記載したVSCode上でマウスオーバーした際にポップアップ表示される型情報を記載しています。

コンポーネント定義の箇所は、
Register: React.FC<RegisterProps> と、RegisterPropsの型定義を持つReact.FCと言う型情報になります。続いてApp.tsxより渡されるProps、loading, newCard, setNewCard, setUserid, insertDb, userCheckDb を定義。
更に、Chakra UIのトースト機能、React RouterのuseNavigateの定義を行っています。

次に、Inputフィールド、Textareaフィールド、ボタンクリック時の処理についてです。

// /src/components/Register.tsx

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    //inputフィールドにデータが入力されたら、 setNewCardで値名:値でデータ格納
    const { name, value } = e.target;
    setNewCard({
        ...newCard,
        [name]: value,
    });
};

const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    //textareaフィールドにデータが入力されたら、 setNewCardで値名:値でデータ格納
    const { name, value } = e.target;
    setNewCard({
        ...newCard,
        [name]: value,
    });
};

const handleRegister = async () => {//async/awaitによる非同期処理
    if (newCard.user_id && newCard.name && newCard.description && newCard.skill_id) {//必須入力項目が空でなければ
        const isUserIdUnique = await userCheckDb(newCard);//ユーザID重複チェックを実行
        if (isUserIdUnique) {//ユーザIDが重複していなければ
            await insertDb(newCard);//データ新規登録を実行
            setUserid(newCard.user_id);//useridを入力されたuser_idでセット、/に遷移した際に、IDフィールドに予めID情報をセットする為
            setNewCard({//newCardは初期化実施
                user_id: '',
                name: '',
                description: '',
                github_id: '',
                qiita_id: '',
                x_id: '',
                skill_id: 0,
                skills: '',
            });
            navigate('/');// '/'に遷移
        }
    } else {
        toast({
            title: '*の必須項目を入力してください',
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
    }
};

最初の、handleInputChange は、inputフィールドに入力されたデータを、setNewCard で、newCardステートに追加、格納しています。…newCard と「…」を記載してますが、これはJavaScriptのスプレッド構文と呼ばれる記法です。配列やオブジェクトの中身を展開し、値の追加や変換等を行う場合に使われます。

次の、handleTextareaChange も同様です。textareaに入力されたデータを、setNewCard で、newCardステートに追加、格納しています。

最後の、handleRegister は「登録」ボタンをクリックした際に動作する関数です。ここで、入力されたデータ群をSupabaseのDBテーブルに登録します。そのプロセスは、以下のような流れです。

  • 非同期通信、async/awaitで処理開始
  • 必須項目の入力チェック
  • 入力ユーザIDの重複チェック(userCheckDbの実行)
  • 上記問題なければ、Supabaseのテーブルにデータを新規登録(insertDbの実行)
  • 処理後、ルート(/)に遷移した際、IDフィールドに登録したIDを表示する為、useridをセット
  • newCardステートの初期化
  • useNavigateでルート(/)に遷移
  • 必須項目が入力されていなければ、Chakra UIのトーストでエラーメッセージ表示

それでは、最後に、JSXの箇所です。

// /src/components/Register.tsx

return (
    <>
        <Flex alignItems='center' justify='center' p={5}>
            <Card maxW='400px'>
                <CardHeader>
                    <Heading size='md' textAlign='center'>カード登録</Heading>
                </CardHeader>
                <CardBody>
                    <Text>ID名*</Text>
                    <Input
                        autoFocus
                        placeholder='ご希望のID名を入力'
                        name='user_id'
                        value={newCard.user_id}
                        onChange={handleInputChange} //データ入力時、handleInputChange実行
                    />
                    <Text mt='3'>お名前*</Text>
                    <Input
                        placeholder='お名前を入力'
                        name='name'
                        value={newCard.name}
                        onChange={handleInputChange} //データ入力時、handleInputChange実行
                    />
                    <Text mt='3'>自己紹介*</Text>
                    <Textarea
                        placeholder='<h1>HTMLタグも入力できます</h1>'
                        name='description'
                        value={newCard.description}
                        onChange={handleTextareaChange} //データ入力時、handleTextareaChange実行
                    />
                    <Text mt='3'>好きな技術*</Text>
                    <Select
                        placeholder='選択してください'
                        name='skill_id'
                        value={newCard.skill_id.toString()} // 数値を文字列に変換して表示
                        onChange={(e) => setNewCard({ ...newCard, skill_id: Number(e.target.value) })} //データ入力時、文字列を数値に変換の上、setNewCard実行
                    >
                        <option value='1'>React</option>
                        <option value='2'>TypeScript</option>
                        <option value='3'>GitHub</option>
                    </Select>
                    <Text mt='3'>GitHub ID</Text>
                    <Input
                        name='github_id'
                        value={newCard.github_id}
                        onChange={handleInputChange} //データ入力時、handleInputChange実行
                    />
                    <Text mt='3'>Qiita ID</Text>
                    <Input
                        name='qiita_id'
                        value={newCard.qiita_id}
                        onChange={handleInputChange} //データ入力時、handleInputChange実行
                    />
                    <Text mt='3'>X(Twitter) ID</Text>
                    <Input
                        name='x_id'
                        value={newCard.x_id}
                        onChange={handleInputChange} //データ入力時、handleInputChange実行
                    />
                    <Box mt={4} textAlign='center'>
                        <Button
                            isLoading={loading}
                            loadingText='Loading'
                            colorScheme='blue'
                            spinnerPlacement='start'
                            mr='5'
                            onClick={handleRegister} //クリック時、handleRegister実行
                        >登録</Button>
                        <Button
                            colorScheme='gray'
                            variant='outline'
                            onClick={() => {
                                navigate('/');
                            }
                            }>戻る</Button>
                    </Box>
                </CardBody>
            </Card>
        </Flex >
    </>
)

Chakra UIのコンポーネントを色々配置してます。Inputの箇所はコメント記載の通り、入力値をhandleInputChangeにより、setNewCard で、newCardステートに追加、格納しています。
Textaeraも同様です。Selectの箇所は、選択肢はそれぞれ、valueが数値型(number)ですが、valueは一度、文字列(string)に変換しないとうまくいかない為、変換を実施しています。setNewCardの処理で再度number型に戻して格納しています。

最後に、「登録」ボタンクリックで、handleRegisterを実行、Supabaseテーブルへのデータ新規登録処理を実行します。

この時点で、Home画面の新規登録ボタンをクリックすると、Registerに遷移し、下記画面が表示されると思います。

5.4 新規登録機能の実装

それでは、App.tsxに仮設置している、insertDb、userCheckDbの処理を作成していきます。
まずは、insertDbです。

// /src/App.tsx

//DBへの新規データ登録
// Step 1: usersへの登録
const insertDb = async (card: CardData): Promise<boolean> => { //async/awaitによる非同期処理
    setLoading(true);//ローディングをローディング中に設置
    const { data: userData, error: userError } = await supabase //Supabaseのinsert(データ挿入)処理を実行
        .from('users')//usersテーブルに対して
        .insert([//下記項目を挿入
            {
                user_id: card.user_id,
                name: card.name,
                description: card.description,
                github_id: card.github_id,
                qiita_id: card.qiita_id,
                x_id: card.x_id,
            },
        ])
        .select();
    if (userError || !userData) {//エラーが発生もしくは、userDataが空の場合
        console.error('Error insert data:', userError);
        toast({ //Chakra UIのトーストにてエラーメッセージ表示
            title: 'ユーザ登録が失敗しました',
            description: `${userError}`,
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }
    console.log('insertDb step1', userData, userError)//登録したuserDataをコンソール出力

    // Step 2: user_skillテーブルへの登録
    const { data: userSkillData, error: userSkillError } = await supabase //Supabaseのinsert(データ挿入)処理を実行
        .from('user_skill')//user_skillテーブルに対して
        .insert([//下記項目を挿入
            {
                user_id: card.user_id,
                skill_id: card.skill_id,
            },
        ])
        .select();
    if (userSkillError || !userSkillData) {//エラーが発生もしくは、userSkillDataが空の場合
        console.error('Error insert data:', userSkillError);
        toast({ //Chakra UIのトーストにてエラーメッセージ表示
            title: 'ユーザ登録が失敗しました',
            description: `${userSkillError}`,
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }
    setLoading(false);//正常処理がされればローティング状態を解除
    toast({//Chakra UIのトーストにて成功メッセージ表示
        title: 'ユーザ登録に成功しました',
        position: 'top',
        status: 'success',
        duration: 2000,
        isClosable: true,
    })
    console.log('iinsertDb step2', userSkillData);//登録したuserSkillDataをコンソール出力
    return true;//trueをリターン
}

流れは、selectDbと同様に、複数のテーブルに対しての処理です。
Step1として、usersテーブルへのinsert(データ挿入)処理を実行しています。この処理で、usersテーブルに、user_id, name, description, github_id, qiita_id, x_id を登録しています。
プロセスは、以下の流れです。

  • ローディング状態をローディング中にセット
  • usrsテーブルへのデータ挿入処理を実施
  • エラー発生すれば、Chakra UIのトーストでエラー表示、ローディング状態を解除し、falseリターンで処理終了
  • 正常終了すれば、ローディング状態を解除し、Chakra UIのトーストで成功メッセージ表示、trueをリターン

Step1の処理後、Step2でuser_skillテーブルへのinsert処理を実行します。
user_skillテーブルに、user_id, skill_id を登録しています。
プロセスは、Step1と同じです。
Step1、2共に処理終了後コンソール出力しています。

続いて、ID重複チェックのuserCheckDbです。
IDの重複チェックを実施しなくてもID重複の場合は、Supabase側でエラーとなるので、そのままエラー処理となるだけですが、エラーメッセージが分かりづらいため、敢えて、この処理を設けています。

// /src/App.tsx

//登録ユーザの重複チェック
const userCheckDb = async (card: CardData): Promise<boolean> => {//async/awaitによる非同期処理
    setLoading(true);//ローディングをローディング中にセット
    const { data: userData, error: userError } = await supabase //Supabaseのselect(データ検索)処理を実行
        .from('users')//usersテーブルに対して
        .select('*')
        .eq('user_id', card.user_id);//user_idがcard.user_idのデータを抽出

    if (userData && userData.length > 0) {//取得データがある、もしくは、空ではない場合
        toast({//重複ユーザIDありとして、Chakra UIのトーストにてエラーメッセージ表示
            title: '既にIDが使われています',
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        console.error('既にIDが使われています:', userError, userData);
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }

    setLoading(false);//正常処理した場合ローティング状態を解除
    return true;//trueをリターン
}

他のDB処理と同様、async/awaitによる非同期処理での実装です。
まず、ローディングをローディング中にセットし、Supabaseのselect(データ検索)処理を実行してます。usersテーブルに対して、user_idがcard.user_idのデータの検索です。
ここで該当するuser_idがある場合はID重複となりますので、Chakra UIのトーストにてID重複のエラーメッセージ表示してます。重複の場合は、ローティング状態を解除し、falseをリターンしています。
重複がない場合は、ローティング状態を解除の上、trueをリターンしています。

これで実装完了です。この時点で以下画面のような処理ができます。

6. 編集機能

続いては、名刺データの編集機能を作成していきます。Cardsコンポーネントから、編集ボタンをクリックすると、モーダルが表示され、名刺データの編集が出来るコンポーネント、Editを実装していきます。componentsフォルダに、Edit.tsxを作成します。Edit.tsxのコードは後ほど記載します。その前に関連コンポーネントを変更していきます。

6.1 App.tsxの修正

Edit.tsxの実装に当たり、まずは、App.tsxを修正していきます。
Editコンポーネントは、Cardsコンポーネントからモーダルとして呼び出される為、App.tsx冒頭のインポート箇所や、ステート定義箇所の変更はありません。
まず、SupabaseのDBテーブル編集機能として、updateDbを定義します。

  // /src/App.tsx
  
  //登録ユーザの重複チェック
  const userCheckDb = async (card: CardData): Promise<boolean> => {
   .
   .
   .
  }
  
  //DBの更新処理追加
  const updateDb = async (card: CardData): Promise<boolean> => {//仮設置
    return true;
  }

5章で作成した、userCheckDbの下くらいに、DBの更新処理機能、updateDbを追加します。
これまでと同様、この段階では一旦仮設置とし、具体的な処理は記載してません。trueを返すのみです。後ほど、具体的な処理内容は記載します。

続いて、JSXの箇所です。

// /src/App.tsx

return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb} />
        //Cardsに渡すProps追加、loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb}
        } />
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
      </Routes>
    </>
  )

Editコンポーネントは、前述の通り、Cardコンポーネントから呼び出されますので、AppからはCardsにPropsを渡します。その追加Propsの定義を加えます。
loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb}
です。

現時点でのApp.tsxのコード全文は以下となります。

// /src/App.tsx

import { useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom"
import { useToast } from "@chakra-ui/react";
import Home from "./components/Home"
import Cards from "./components/Cards";
import { CardData } from "./cardData";
import { supabase } from "./supabaseClient";
import Register from "./components/Register";

function App() {
  const [userid, setUserid] = useState<string>();
  const [cardData, setCardData] = useState<CardData[]>([]);
  const [newCard, setNewCard] = useState<CardData>({
    user_id: '',
    name: '',
    description: '',
    github_id: '',
    qiita_id: '',
    x_id: '',
    skill_id: 0,
    skills: '',
  });
  const [loading, setLoading] = useState<boolean>(false);
  const toast = useToast()
  const navigate = useNavigate()

  // Supabaseからデータを取得する関数
  const selectDb = async (): Promise<boolean> => {
    setLoading(true);

    // Step 1: usersテーブルからuser_idを取得
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', userid);

    if (userError || !userData || userData.length === 0) {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user data:', userError);
      setLoading(false);
      return false;
    }
    console.log('step1', userData)

    // Step 2: user_skillテーブルから該当するskill_idを取得
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .select('skill_id')
      .eq('user_id', userid);

    if (userSkillError || !userSkillData || userSkillData.length === 0) {
      toast({
        title: 'Skill IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user skills:', userSkillError);
      setLoading(false);
      return false;
    }
    console.log('step2', userSkillData)
    const skill_ids = userSkillData.map((skill) => skill.skill_id);

    // Step 3: skillsテーブルからnameを取得
    const { data: skillsData, error: skillsError } = await supabase
      .from('skills')
      .select('name')
      .eq('id', skill_ids);

    if (skillsError || !skillsData || skillsData.length === 0) {
      toast({
        title: 'skillsが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching skills:', skillsError);
      setLoading(false);
      return false;
    }

    console.log('step3', skillsData)
    const skillsString = skillsData.map(skill => skill.name);

    // Step4:結果をcombinedDataに集約
    const combinedData = userData.map((user) => ({
      ...user, skill_id: skill_ids, skills: skillsString,
    }));

    console.log('step4', combinedData)
    setCardData(combinedData);
    toast({
      title: 'データを取得しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    setLoading(false);
    return true;  // データ取得に成功した場合はtrueを返す
  };

  //DBへの新規データ登録
  // Step 1: usersへの登録
  const insertDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    const { data: userData, error: userError } = await supabase
      .from('users')
      .insert([
        {
          user_id: card.user_id,
          name: card.name,
          description: card.description,
          github_id: card.github_id,
          qiita_id: card.qiita_id,
          x_id: card.x_id,
        },
      ])
      .select();
    if (userError || !userData) {
      console.error('Error insert data:', userError);
      toast({
        title: 'ユーザ登録が失敗しました',
        description: `${userError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      setLoading(false);
      return false;
    }
    console.log('insertDb step1', userData, userError)
    // Step 2: user_skillテーブルへの登録
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .insert([
        {
          user_id: card.user_id,
          skill_id: card.skill_id,
        },
      ])
      .select();
    if (userSkillError || !userSkillData) {
      console.error('Error insert data:', userSkillError);
      toast({
        title: 'ユーザ登録が失敗しました',
        description: `${userSkillError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      setLoading(false);
      return false;
    }
    setLoading(false);
    toast({
      title: 'ユーザ登録に成功しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    console.log('iinsertDb step2', userSkillData);
    return true;
  }

  //登録ユーザの重複チェック
  const userCheckDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', card.user_id);

    if (userData && userData.length > 0) {
      toast({
        title: '既にIDが使われています',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('既にIDが使われています:', userError, userData);
      setLoading(false);
      return false;
    }

    setLoading(false);
    return true;
  }

  //DBの更新処理
  const updateDb = async (card: CardData): Promise<boolean> => {//仮設置
    return true;
  }

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUserid(e.target.value)
  }

  const handleSearch = async () => {
    if (userid) {
      const success = await selectDb();
      if (success) {
        navigate(`/card/${userid}`);
      }
    } else {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
    }
  };

  return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb} />//Cardsに渡すProps追加
        } />
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
      </Routes>
    </>
  )
}

export default App

6.2 Card.tsxの修正

続いて、Cardコンポーネントを修正していきます。EditコンポーネントはCardsコンポーネントにモーダルの形で呼び出される為、Editコンポーネントに渡すPropsはCardsコンポーネント経由で渡されます。

下記は、Card.tsxのコード全文です。

// /src/components/Cards.tsx
import { Link, useNavigate } from "react-router-dom";
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Icon, Text } from "@chakra-ui/react";
import { CardData } from "../cardData";
import { FaGithub } from "react-icons/fa";
import { SiQiita } from "react-icons/si";
import { FaXTwitter } from "react-icons/fa6";
import Edit from "./Edit";//Editコンポーネントのインポート

type CardsProps = {
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    loading: boolean;//Editに渡すため追加
    setLoading: React.Dispatch<React.SetStateAction<boolean>>;//Editに渡すため追加
    updateDb: (card: CardData) => Promise<boolean>;//Editに渡すため追加
    selectDb: (user_id: string) => Promise<boolean>;//Editに渡すため追加
}

const Cards: React.FC<CardsProps> = ({ cardData, setCardData, loading, setLoading, updateDb, selectDb }) => { //loading, setLoading, updateDb, selectDb追加
    const navigate = useNavigate();

    return (
        <Flex alignItems='center' justify='center' p={5}>
            <Card maxW='400px'>
                <CardHeader>
                    <Heading size='md' textAlign='center'>Name Card App</Heading>
                </CardHeader>
                <CardBody>
                    {cardData.map((card, index) => (
                        <div key={index}>
                            <Box borderWidth='1px' borderRadius='lg' p={5}>
                                <Heading size='sm' textTransform='uppercase'>
                                    ID
                                </Heading>
                                <Text pb='2' fontSize='sm'>{card.user_id}</Text>
                                <Heading size='sm' textTransform='uppercase'>
                                    名前
                                </Heading>
                                <Text pb='2' fontSize='sm'>{card.name}</Text>
                                <Heading size='sm' textTransform='uppercase'>
                                    自己紹介
                                </Heading>
                                <Text pb='2'
                                    dangerouslySetInnerHTML={{ __html: card.description }}
                                />
                                <Heading size='sm' textTransform='uppercase'>
                                    好きな技術
                                </Heading>
                                <Text pb='2' fontSize='sm'>{card.skills}</Text>
                                <Flex wrap='nowrap' justifyContent='center' width='100%' >
                                    <Link to={`https://github.com/${card.github_id}`} target='_blank'>
                                        <Icon
                                            as={FaGithub}
                                            fontSize="24px"
                                            margin={1}
                                            _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                        />
                                    </Link>
                                    <Link to={`https://qiita.com/${card.qiita_id}`} target='_blank'>
                                        <Icon
                                            as={SiQiita}
                                            fontSize="24px"
                                            margin={1}
                                            _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                        />
                                    </Link>
                                    <Link to={`https://x.com/${card.x_id}`} target='_blank'>
                                        <Icon
                                            as={FaXTwitter}
                                            fontSize="24px"
                                            margin={1}
                                            _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                        />
                                    </Link>
                                </Flex>
                            </Box>
                        </div>
                    ))}
                    <Box mt={4} textAlign='center'>
                        {/*削除ここから
                        <Button
                            colorScheme='blue'
                            mr='3'
                        >編集
                        </Button>
                        削除ここまで*/}

                        {/*追加ここから*/}
                        <Edit loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} updateDb={updateDb} selectDb={selectDb} />
                        {/*追加ここまで*/}

                        <Button
                            colorScheme='orange'
                            variant='outline'
                            mr='3'>
                            削除</Button>
                        {/*
                        <Delete error={error} setError={setError} loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} deleteDb={deleteDb}/>
                        */}
                        <Button
                            colorScheme='gray'
                            variant='outline'
                            onClick={() => {
                                setCardData([]);
                                navigate('/');
                            }
                            }>戻る</Button>
                    </Box>
                </CardBody>
            </Card>
        </Flex>
    )
}

export default Cards;

解説していきます。
まず、冒頭のインポート及び型定義、コンポーネント定義の箇所です。

// /src/components/Cards.tsx

import { Link, useNavigate } from "react-router-dom";
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Icon, Text } from "@chakra-ui/react";
import { CardData } from "../cardData";
import { FaGithub } from "react-icons/fa";
import { SiQiita } from "react-icons/si";
import { FaXTwitter } from "react-icons/fa6";
import Edit from "./Edit";//Editコンポーネントのインポート

type CardsProps = {
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    loading: boolean;//Editに渡すため追加
    setLoading: React.Dispatch<React.SetStateAction<boolean>>;//Editに渡すため追加
    updateDb: (card: CardData) => Promise<boolean>;//Editに渡すため追加
    selectDb: (user_id: string) => Promise<boolean>;//Editに渡すため追加
}

const Cards: React.FC<CardsProps> = ({ cardData, setCardData, loading, setLoading, updateDb, selectDb }) => { //loading, setLoading, updateDb, selectDb追加
    const navigate = useNavigate();

インポートの箇所は、新たにEditコンポーネントのインポートを追加しています。
次に型定義ですが、Cards経由でEditにPropsを渡しますので、その為の型定義を追加しています。
loading, setLoading, updateDb, selectDb の型定義です。それぞれこれまでと同じ要領で、追加します。
selectDbはDB更新(updateDb)後、DBテーブルデータの再取得を行うために、渡しています。
続いて、コンポーネントの定義の箇所は、受け取るEdit用のPropsとして、型定義と同様、loading, setLoading, updateDb, selectDbを追加しています。

次は、JSX箇所です。

// /src/components/Cards.tsx

return (
    <Flex alignItems='center' justify='center' p={5}>
        <Card maxW='400px'>
            <CardHeader>
                <Heading size='md' textAlign='center'>Name Card App</Heading>
            </CardHeader>
            <CardBody>
                {cardData.map((card, index) => (
                    <div key={index}>
                        <Box borderWidth='1px' borderRadius='lg' p={5}>
                            <Heading size='sm' textTransform='uppercase'>
                                ID
                            </Heading>
                            <Text pb='2' fontSize='sm'>{card.user_id}</Text>
                            <Heading size='sm' textTransform='uppercase'>
                                名前
                            </Heading>
                            <Text pb='2' fontSize='sm'>{card.name}</Text>
                            <Heading size='sm' textTransform='uppercase'>
                                自己紹介
                            </Heading>
                            <Text pb='2'
                                dangerouslySetInnerHTML={{ __html: card.description }}
                            />
                            <Heading size='sm' textTransform='uppercase'>
                                好きな技術
                            </Heading>
                            <Text pb='2' fontSize='sm'>{card.skills}</Text>
                            <Flex wrap='nowrap' justifyContent='center' width='100%' >
                                <Link to={`https://github.com/${card.github_id}`} target='_blank'>
                                    <Icon
                                        as={FaGithub}
                                        fontSize="24px"
                                        margin={1}
                                        _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                    />
                                </Link>
                                <Link to={`https://qiita.com/${card.qiita_id}`} target='_blank'>
                                    <Icon
                                        as={SiQiita}
                                        fontSize="24px"
                                        margin={1}
                                        _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                    />
                                </Link>
                                <Link to={`https://x.com/${card.x_id}`} target='_blank'>
                                    <Icon
                                        as={FaXTwitter}
                                        fontSize="24px"
                                        margin={1}
                                        _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                    />
                                </Link>
                            </Flex>
                        </Box>
                    </div>
                ))}
                <Box mt={4} textAlign='center'>
                    {/*削除ここから、これまでの「編集」ボタンはEditコンポーネントと入れ替える為、削除
                    <Button
                        colorScheme='blue'
                        mr='3'
                    >編集
                    </Button>
                    削除ここまで*/}

                    {/*追加ここから、Editコンポーネントのセット*/}
                    <Edit loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} updateDb={updateDb} selectDb={selectDb} />
                    {/*追加ここまで*/}

                    <Button
                        colorScheme='orange'
                        variant='outline'
                        mr='3'>
                        削除</Button>
                    <Button
                        colorScheme='gray'
                        variant='outline'
                        onClick={() => {
                            setCardData([]);
                            navigate('/');
                        }
                        }>戻る</Button>
                </Box>
            </CardBody>
        </Card>
    </Flex>
)

JSXの箇所は、これまで「編集」ボタンとして、<Button>を配置していた箇所を、Editコンポーネントの配置に変更します。ボタン部分を削除して、新たに<Edit …/>を追加します。
<Editに渡すPropsは、冒頭の型定義と同様、
loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} updateDb={updateDb} selectDb={selectDb}
となります。

6.3 Edit.tsxの作成

では、Edit.tsxにコードを具体的に記載していきます。
以下は、Edit.tsxのコード全文です。

// /src/components/Edit.tsx
import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Select, Textarea, useDisclosure, useToast } from "@chakra-ui/react";
import { useRef, useState, useEffect } from "react";
import { CardData } from "../cardData";

type EditProps = {
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    loading: boolean;
    setLoading: React.Dispatch<React.SetStateAction<boolean>>
    updateDb: (card: CardData) => Promise<boolean>;
    selectDb: (user_id: string) => Promise<boolean>;

}

const Edit: React.FC<EditProps> = ({ cardData, loading, updateDb, selectDb }) => {
    const { isOpen, onOpen, onClose } = useDisclosure()
    const initialRef = useRef(null)
    const finalRef = useRef(null)
    const [editCard, setEditCard] = useState<CardData>(
        {
            user_id: '',
            name: '',
            description: '',
            github_id: '',
            qiita_id: '',
            x_id: '',
            skill_id: 0,
            skills: ''
        });
    const toast = useToast()

    useEffect(() => {
        if (cardData && cardData.length > 0) {
            setEditCard(cardData[0]); // cardDataが存在する場合にのみセット
        }
    }, [cardData]);

    const handleOpen = () => {
        if (cardData && cardData.length > 0) {
            setEditCard(cardData[0]); // cardDataが存在する場合にのみセット
        }
        onOpen();
    };

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setEditCard({
            ...editCard,
            [name]: value,
        });
    };

    const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        const { name, value } = e.target;
        setEditCard({
            ...editCard,
            [name]: value,
        });
    };

    const handleUpdate = async () => {
        if (editCard.name && editCard.description && editCard.skill_id) {
            const isUpdate = await updateDb(editCard);
            if (isUpdate) {
                await selectDb(editCard.user_id);//更新後のモダールクローズ後にskill情報を表示させるために、selectDbを実行
            }
            else {
                toast({
                    title: '更新に失敗しました',
                    position: 'top',
                    status: 'error',
                    duration: 2000,
                    isClosable: true,
                })
            }
            console.log('editCard', editCard);
            if (!loading) {
                setTimeout(() => {
                    onClose();
                }, 500);
            }
        }
        else {
            toast({
                title: '*の必須項目を入力してください',
                position: 'top',
                status: 'error',
                duration: 2000,
                isClosable: true,
            })
        }
    }

    return (
        <>
            <Button //Cards上に表示されるボタン部部分
                loadingText='Loading'
                colorScheme='blue'
                spinnerPlacement='start'
                mr='3'
                onClick={handleOpen}
            >編集
            </Button>
            <Modal //ここからモーダル箇所
                initialFocusRef={initialRef}
                finalFocusRef={finalRef}
                isOpen={isOpen}
                onClose={onClose}
            >
                <ModalOverlay />
                <ModalContent>
                    <ModalHeader>Name Card 編集</ModalHeader>
                    <ModalCloseButton />
                    <ModalBody pb={6}>
                        <FormControl mb='3'>
                            <FormLabel>ID変更できません)</FormLabel>
                            <Input
                                placeholder='IDは変更できません'
                                value={editCard.user_id || ''} // 空文字列をデフォルトに
                                isDisabled
                            />
                        </FormControl>
                        <FormControl mb='3'>
                            <FormLabel>お名前*</FormLabel>
                            <Input
                                ref={initialRef}
                                placeholder='お名前'
                                name="name"
                                value={editCard.name}
                                onChange={handleInputChange}
                            />
                        </FormControl>
                        <FormControl mb='3'>
                            <FormLabel>自己紹介*</FormLabel>
                            <Textarea
                                placeholder='<h1>HTMLタグも入力できます</h1>'
                                name='description'
                                value={editCard.description}
                                onChange={handleTextareaChange}
                            />
                        </FormControl>
                        <FormControl mb='3'>
                            <FormLabel>好きな技術*</FormLabel>
                            <Select
                                name='skill_id'
                                value={String(editCard.skill_id)} // 数値を文字列に変換
                                onChange={(e) => setEditCard({ ...editCard, skill_id: Number(e.target.value) })}//数値に変換し直してセット
                                placeholder='選択してください'
                            >
                                <option value='1'>React</option>
                                <option value='2'>TypeScript</option>
                                <option value='3'>GitHub</option>
                            </Select>
                        </FormControl>
                        <FormControl mb='3'>
                            <FormLabel>GitHub ID</FormLabel>
                            <Input
                                name='github_id'
                                value={editCard.github_id}
                                onChange={handleInputChange}
                            />
                        </FormControl>
                        <FormControl mb='3'>
                            <FormLabel>Qiita ID</FormLabel>
                            <Input
                                name='qiita_id'
                                value={editCard.qiita_id}
                                onChange={handleInputChange}
                            />
                        </FormControl>
                        <FormControl mb='3'>
                            <FormLabel>X(Twitter) ID</FormLabel>
                            <Input
                                name='x_id'
                                value={editCard.x_id}
                                onChange={handleInputChange}
                            />
                        </FormControl>
                    </ModalBody>
                    <ModalFooter>
                        <Button
                            isLoading={loading}
                            loadingText='Loading'
                            colorScheme='blue'
                            spinnerPlacement='start'
                            mr={3}
                            onClick={handleUpdate}
                        >
                            データを更新
                        </Button>
                        <Button
                            variant='outline'
                            onClick={onClose}>Cancel</Button>
                    </ModalFooter>
                </ModalContent>
            </Modal>
        </>
    )
}
export default Edit;

それでは解説していきます。
まず冒頭のインポート、コンポーネント定義の箇所です。

// /src/components/Edit.tsx
import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Select, Textarea, useDisclosure, useToast } from "@chakra-ui/react"; //Chakra UIのインポート
import { useRef, useState, useEffect } from "react";//useRef, useState, useEffectのインポート、useRefはChakra UIのModalで使用
import { CardData } from "../cardData";//CardData型定義のインポート

type EditProps = {//Propsの型定義
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    loading: boolean;
    setLoading: React.Dispatch<React.SetStateAction<boolean>>
    updateDb: (card: CardData) => Promise<boolean>;
    selectDb: (user_id: string) => Promise<boolean>;

}

const Edit: React.FC<EditProps> = ({ cardData, loading, updateDb, selectDb }) => {
    const { isOpen, onOpen, onClose } = useDisclosure()//Chakra UIのModal用の定義
    const initialRef = useRef(null)//Chakra UIのModal用の定義
    const finalRef = useRef(null)//Chakra UIのModal用の定義
    const [editCard, setEditCard] = useState<CardData>(//Edit用のstate, editCardを定義
        {
            user_id: '',
            name: '',
            description: '',
            github_id: '',
            qiita_id: '',
            x_id: '',
            skill_id: 0,
            skills: ''
        });
    const toast = useToast()//Chakra UIのToast機能の定義

インポートについては、記載の通りです。Chakra UI, React, CardDataの型情報のインポートを行っています。ReactのuseRefはChakra UIのModalで使用します。

type設定については、Cards.tsxからcardData, loading, updateDb, selectDbがPropsとして渡されますので、その定義を行っています。newCardは、CardData(/src/cardData.ts)で定義された型情報を持ったオブジェクトです。
set関数は、前編4.2で記載したVSCode上でマウスオーバーした際にポップアップ表示される型情報を記載しています。
updateDb, selectDbは、App.tsxからの関数Propsです。型定義はこのようなアロー関数の形となります。なお、selectDについては、引数を明示的に指定したかった為、selectDb: (user_id: string)と定義しています。

コンポーネント定義の箇所は、これまでと同様、React.FC<EditProps>と型定義し、Cardsより渡されるPropsを設定しています。cardData, loading, updateDb, selectDb です。
その下の、const設定は、Chakra UIのModal用の定義をしています。

また、Editコンポーネントのローカルステートとして、editCardをセットしています。これは5章のRegisterコンポーネントのnewCardステートと同じ考えです。
また、Chakra UIのToast機能の定義をしています。

続いて、コンポーネント起動時、Inputフィールド、Textareaフィールド、ボタンクリック時の処理についてです。

// /src/components/Edit.tsx

useEffect(() => {
    if (cardData && cardData.length > 0) {
        setEditCard(cardData[0]); // editCardに、cardData配列の1番目をセット。cardDataが存在する場合にのみセット
    }
}, [cardData]);//cardData変化時に実行

const handleOpen = () => {//モーダルオープン時に実行
    if (cardData && cardData.length > 0) {
        setEditCard(cardData[0]); // editCardに、cardData配列の1番目をセット。cardDataが存在する場合にのみセット
    }
    onOpen();
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    //inputフィールドにデータが入力されたら、 setNewCardで値名:値でデータ格納
    const { name, value } = e.target;
    setEditCard({
        ...editCard,
        [name]: value,
    });
};

const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    //textareaフィールドにデータが入力されたら、 setNewCardで値名:値でデータ格納
    const { name, value } = e.target;
    setEditCard({
        ...editCard,
        [name]: value,
    });
};

const handleUpdate = async () => {//async/awaitによる非同期処理
    if (editCard.name && editCard.description && editCard.skill_id) {//必須入力項目が空でなければ
        const isUpdate = await updateDb(editCard);//DB更新処理を実行
        if (isUpdate) {//DB更新が正常処理されれば
            await selectDb(editCard.user_id);//更新後のモダールクローズ後にskill情報を表示させるために、selectDbを実行
        }
        else {
            toast({//エラー時は、Chakra UIのToastでエラー表示
                title: '更新に失敗しました',
                position: 'top',
                status: 'error',
                duration: 2000,
                isClosable: true,
            })
        }
        console.log('editCard', editCard);//更新内容をコンソール出力
        if (!loading) {//ローディング解除されていれば、0.5秒後、モーダルをクローズ
            setTimeout(() => {
                onClose();
            }, 500);
        }
    }
    else {
        toast({//必須入力項目が空の場合は、Chakra UIのToastでエラー表示
            title: '*の必須項目を入力してください',
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
    }
}

冒頭、Reactの機能(hook)である、useEffectを利用しています。
useEffectは、コンポーネントマウント時、stateの更新時など、特定の条件下で処理(副作用 =effect)を行うものです。

ここでは、Editコンポーネントマウント時、cardDataに値があれば(Supabaseよりデータを読み込んでいれば)、editCardにステートにデータをセットする処理を行っています。これは、Editコンポーネントを表示した際に、Cardsコンポーネントで表示していた、テーブルデータをそのまま編集用の初期値としてinputやtextareaフィールド等、各UIパーツにデータをセットする為です。
cardDataは配列の為、かつ配列は1組分しか入っていない為、cardData[0]の値をセットしています。

次に、handleOpen関数でEditコンポーネントのモーダルを開いた際の処理を記述しています。内容は、先のuseEffectの処理と同一です。これはどうもモーダルを開いた時に、意図通りのデータがセットされないケースがあった為、モーダルオープン時にも明示的にeditDataをセットする処理としました。

続く、handleInputChange、handleTextareaChange は、Registerコンポーネントと同じです。
input、textareaフィールドに入力されたデータを、setNewCard で、スプレッド構文により、newCardステートに追加、格納しています。

最後のhandleUpdateが、DBのデータ更新処理です。「データを更新」ボタンクリックで処理されます。Registerコンポーネント時と同じような流れです。

  • 非同期通信、async/awaitで処理開始
  • 必須項目の入力チェック
  • DBの更新処理(updateDbの実行)
  • 正常に処理されれば、モーダルクローズ時のCardsコンポーネントにて、データ表示する為、Supabaseテーブルの読込処理(selectDbの実行)
  • DB処理エラーの場合は、Chakra UIのトーストでエラーメッセージ表示
  • 処理後、ローディング状態(これはupdateDb、selectDbの処理で制御されます)が解除されていればモーダルをクローズ
  • 必須項目が入力されていなければ、Chakra UIのトーストでエラーメッセージ表示

最後にJSXの箇所です。

// /src/components/Edit.tsx

return (
    <>
        <Button //Cards上に表示されるボタン部部分
            loadingText='Loading'
            colorScheme='blue'
            spinnerPlacement='start'
            mr='3'
            onClick={handleOpen}//クリックでhandleOpen実行、モーダルオープン
        >編集
        </Button>
        <Modal //ここからモーダル箇所
            initialFocusRef={initialRef}
            finalFocusRef={finalRef}
            isOpen={isOpen}
            onClose={onClose}
        >
            <ModalOverlay />
            <ModalContent>
                <ModalHeader>Name Card 編集</ModalHeader>
                <ModalCloseButton />
                <ModalBody pb={6}>
                    <FormControl mb='3'>
                        <FormLabel>ID変更できません)</FormLabel>
                        <Input
                            placeholder='IDは変更できません'
                            value={editCard.user_id || ''} // user_idはDBの更新キーのため、編集不可に。空文字列をデフォルトに
                            isDisabled
                        />
                    </FormControl>
                    <FormControl mb='3'>
                        <FormLabel>お名前*</FormLabel>
                        <Input
                            ref={initialRef}
                            placeholder='お名前'
                            name="name"
                            value={editCard.name}
                            onChange={handleInputChange}//データ入力時、handleInputChange実行
                        />
                    </FormControl>
                    <FormControl mb='3'>
                        <FormLabel>自己紹介*</FormLabel>
                        <Textarea
                            placeholder='<h1>HTMLタグも入力できます</h1>'
                            name='description'
                            value={editCard.description}
                            onChange={handleTextareaChange}//データ入力時、handleTextareaChange実行
                        />
                    </FormControl>
                    <FormControl mb='3'>
                        <FormLabel>好きな技術*</FormLabel>
                        <Select
                            name='skill_id'
                            value={String(editCard.skill_id)} // 数値を文字列に変換
                            onChange={(e) => setEditCard({ ...editCard, skill_id: Number(e.target.value) })}//データ入力時、文字列を数値に変換の上、setEditCard実行
                            placeholder='選択してください'
                        >
                            <option value='1'>React</option>
                            <option value='2'>TypeScript</option>
                            <option value='3'>GitHub</option>
                        </Select>
                    </FormControl>
                    <FormControl mb='3'>
                        <FormLabel>GitHub ID</FormLabel>
                        <Input
                            name='github_id'
                            value={editCard.github_id}
                            onChange={handleInputChange}//データ入力時、handleInputChange実行
                        />
                    </FormControl>
                    <FormControl mb='3'>
                        <FormLabel>Qiita ID</FormLabel>
                        <Input
                            name='qiita_id'
                            value={editCard.qiita_id}
                            onChange={handleInputChange}//データ入力時、handleInputChange実行
                        />
                    </FormControl>
                    <FormControl mb='3'>
                        <FormLabel>X(Twitter) ID</FormLabel>
                        <Input
                            name='x_id'
                            value={editCard.x_id}
                            onChange={handleInputChange}//データ入力時、handleInputChange実行
                        />
                    </FormControl>
                </ModalBody>
                <ModalFooter>
                    <Button
                        isLoading={loading}
                        loadingText='Loading'
                        colorScheme='blue'
                        spinnerPlacement='start'
                        mr={3}
                        onClick={handleUpdate}//クリック時、handleUpdate実行
                    >
                        データを更新
                    </Button>
                    <Button
                        variant='outline'
                        onClick={onClose}//クリック時、モーダルクローズ
                    >Cancel</Button>
                </ModalFooter>
            </ModalContent>
        </Modal>
    </>
)

冒頭の<Button>の箇所は、Cardsコンポーネントに表示される、「編集」ボタンの箇所です。クリックすると、handleOpenが実行され、モーダルがオープンします。
その下の<Modal>の箇所からがモーダルで表示される内容です。

内容はRegisterコンポーネントと同じ感じです。Chakra UIのコンポーネントによる、Inputの箇所はコメント記載の通り、入力値をhandleInputChangeにより、setEditCard で、editCardステートに追加、格納しています。
Textaeraも同様です。Selectの箇所は、選択肢はそれぞれ、valueが数値型(number)ですが、valueは一度、文字列(string)に変換しないとうまくいかない為、変換を実施しています。setEditCardの処理で再度number型に戻して格納しています。

最後に、「データ更新」ボタンクリックで、handleUpdateを実行、Supabaseテーブルへのデータ更新処理を実行します。

この時点でCardsコンポーネントから、「編集」ボタンをクリックすると以下のような編集画面が表示されると思います。

6.4 データ更新機能の実装

それでは、App.tsxに仮設置してる、updateDbの処理を作成していきます。
以下、updateDbの全体です。

// /src/App.tsx

//DBの更新処理
const updateDb = async (card: CardData): Promise<boolean> => {//async/awaitによる非同期処理
    setLoading(true);//ローディングをローディング中にセット
    //step1:usersテーブルの更新
    const { data: userData, error: userError } = await supabase//Supabaseのupdate(データ更新)処理を実行
        .from('users')//usersテーブルに対して
        .update({//下記項目を更新
            name: card.name,
            description: card.description,
            github_id: card.github_id,
            qiita_id: card.qiita_id,
            x_id: card.x_id,
        })
        .eq('user_id', card.user_id)//user_idがcard.user_idのデータに対して更新実施
        .select();
    if (userError || !userData || userData.length === 0) {//エラーあり、もしくは、userDataが無い、または空の場合
        toast({//Chakra UIのトーストにてエラーメッセージ表示
            title: 'IDが見つかりません',
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        console.error('Error fetching user data:', userError);
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }
    console.log('dbUpdate step1', userData)//結果をコンソール出力

    // Step 2: user_skillテーブルへの登録、user_skillテーブルは値が変わってないデータをupdateするとエラーになるため、値が同一かどうかをチェック
    // Step 2-1: 現在のskill_idを取得
    const { data: currentUserSkillData, error: currentUserSkillError } = await supabase//Supabaseのselect(データ検索)処理を実行
        .from('user_skill')//user_skillテーブルに対して
        .select('skill_id')//検索対象、skill_id
        .eq('user_id', card.user_id);//user_idがcard.user_idのデータを検索
    console.log('dbUpdate step2-1', currentUserSkillData)//結果をコンソール出力

    if (currentUserSkillError || !currentUserSkillData) {//エラーあり、もしくは、currentUserSkillDataが無い場合
        toast({//Chakra UIのトーストにてエラーメッセージ表示
            title: 'IDが見つかりません',
            title: '現在の Skill が取得できません',
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        console.error('Error fetching current user skills:', currentUserSkillError);
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }

    /*現在の skill_id と新しい skill_id の配列を比較
    当初は応答からcurrentUserSkillData.skill_idでをskill_id抽出して比較していたが、それだと、skill_idは,
    stringで、比較対象のcard.skill_idは配列だったため、必ず不一致で処理が走っていた。この為、共に配列化して比較する処理に変更
    */
    const currentSkillIds = currentUserSkillData.map((skill) => skill.skill_id);//現在の skill_idをcurrentSkillIdsに配列として格納
    console.log('dbUpdate step2-1比較', currentUserSkillData, currentSkillIds, card.skill_id)
    const newSkillIds = Array.isArray(card.skill_id) ? card.skill_id : [card.skill_id]; // 更新する新しいskill_idが配列でない場合に配列化して、newSkillIdsに格納

    // 現在の skill_id 配列と新しい skill_id 配列が異なるか評価
    const isDifferent = newSkillIds.some((id: number) => !currentSkillIds.includes(id));

    if (isDifferent) {    // 現在の skill_id 配列と新しい skill_id 配列が異なる場合のみ更新
        const { data: userSkillData, error: userSkillError } = await supabase
            .from('user_skill')//user_skillテーブルに対して
            .update({
                skill_id: card.skill_id,//skill_idをcard.skill_idで更新
            })
            .eq('user_id', card.user_id)//user_idが、card.user_idの箇所に対して
            .select();

        if (userSkillError || !userSkillData || userSkillData.length === 0) {//エラーあり、もしくは、userSkillDataが無い、または空の場合
            toast({//Chakra UIのトーストにてエラーメッセージ表示
                title: 'Skill が見つかりません',
                position: 'top',
                status: 'error',
                duration: 2000,
                isClosable: true,
            })
            console.error('Error updating user skills:', userSkillError);
            setLoading(false);//ローティング状態を解除
            return false;//falseをリターン
        }
        console.log('dbUpdate step2', userSkillData);//結果をコンソール出力
    } else {
        console.log('Skill ID は変更されていないため、更新しません');//skill_id更新無しの場合は、その結果をコンソール出力
    }

    setLoading(false);//ローティング状態を解除
    toast({//Chakra UIのトーストにて成功メッセージ表示
        title: '更新が完了しました',
        position: 'top',
        status: 'success',
        duration: 2000,
        isClosable: true,
    })
    return true;//trueをリターン
}

実行している内容について、コメントを細かく記載してます。
データ取得、新規登録処理と同様にStepを設けた処理となっています。
試行錯誤したのは、user_skillテーブルの処理についてです。更新処理は、usersテーブル及びuser_skillテーブルに対して行いますが、user_skillテーブルについては、データ変更がない場合にupdateを実行するとエラーが発生すると言う事象がありました。この為、更新処理しようとしているデータを既存のデータと比較するプロセスを入れてます。かつ、これは配列データでの処理となる為、比較する双方のデータの配列化を行ったうえで、処理を行っています。user_skillテーブルのデータは変更無いと言う判定となれば、user_skillテーブルの更新処理は行わない形となっています。

全体の処理プロセスは以下のような流れです。

  • 非同期通信、async/awaitで処理開始、ローディング状態をローディング中にセット
  • Step1:usersテーブルの更新
    • usersテーブルに対して、user_idがcard.user_idの箇所のデータを更新
    • エラー発生や、結果取得できない場合は、エラー処理(Chkra UIのトースト機能)、ローディング解除、falseリターン、
    • 正常終了であればStep2に推移
  • Step2:user_skillテーブルの更新
    • 現在(更新前)のskill_idをuser_skillテーブルから取得
    • 取得できない場合は、エラー処理(Chkra UIのトースト機能)、ローディング解除、falseリターン
    • 取得できた場合は、取得したskill_idを配列として格納、及び更新しようとしている新しいskill_idを配列に格納
    • 現在のskill_idと新しいskilll_idを比較、評価、異なっている場合のみ、更新処理を実行
    • エラー発生や、結果取得できない場合はエラー処理(Chkra UIのトースト機能)、ローディング解除、falseリターン
    • 正常終了であれば次処理に推移
  • Step1、2、正常終了すれば、ローディング解除、Chakra UIのトーストにて成功メッセージ表示、trueをリターン

ここまでの実装で、以下のような動きが実現できると思います。

7. 削除機能

続いて、名刺データの削除機能です。編集機能と同様、Cardsコンポーネントにモーダルスタイルで実装します。「削除」ボタンをクリックでモーダル表示、削除実施と言った処理を作成していきます。これはDeleteコンポーネントで実現していきます。
componentsフォルダに新たにDelete.tsxを作成します。

Delete.tsxのコードは後ほど記載します。その前に関連コンポーネントを変更していきます。

7.1 App.tsxの修正

まずは、App.tsxを修正していきます。
Deleteコンポーネントは、Editと同様、Cardsコンポーネントからモーダルとして呼び出される為、App.tsx冒頭のインポート箇所や、ステート定義箇所の変更はありません。
まず、SupabaseのDBテーブル編集機能として、deleteDbを定義します。

// /src/App.tsx

  //DBの更新処理
  const updateDb = async (card: CardData): Promise<boolean> => {
  .
  .
  .
  }
  
  //DBの削除処理追加
  const deleteDb = async (userid: string): Promise<boolean> => {//仮設置
    return true;
  }

6章で作成した、updateDbの下くらいに、DBの削除処理機能、deleteDbを追加します。
これまでと同様、この段階では一旦仮設置とし、具体的な処理は記載してません。trueを返すのみです。後ほど、具体的な処理内容は記載します。

続いて、JSXの箇所です。

// /src/App.tsx

  return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb} deleteDb={deleteDb} />
        //Cardsに渡すProps、deleteDb追加
        } />
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
      </Routes>
    </>
  )

Deleteコンポーネントは、Cardコンポーネントから呼び出されますので、AppからはCardsにPropsを渡します。その追加Propsの定義を加えます。
deleteDb={deleteDb} を追加しています。

この時点でのApp.tsxコード全文は以下となります。

// /src/App.tsx

import { useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom"
import { useToast } from "@chakra-ui/react";
import Home from "./components/Home"
import Cards from "./components/Cards";
import { CardData } from "./cardData";
import { supabase } from "./supabaseClient";
import Register from "./components/Register";

function App() {
  const [userid, setUserid] = useState<string>();
  const [cardData, setCardData] = useState<CardData[]>([]);
  const [newCard, setNewCard] = useState<CardData>({
    user_id: '',
    name: '',
    description: '',
    github_id: '',
    qiita_id: '',
    x_id: '',
    skill_id: 0,
    skills: '',
  });
  const [loading, setLoading] = useState<boolean>(false);
  const toast = useToast()
  const navigate = useNavigate()

  // Supabaseからデータを取得する関数
  const selectDb = async (): Promise<boolean> => {
    setLoading(true);

    // Step 1: usersテーブルからuser_idを取得
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', userid);

    if (userError || !userData || userData.length === 0) {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user data:', userError);
      setLoading(false);
      return false;
    }
    console.log('step1', userData)

    // Step 2: user_skillテーブルから該当するskill_idを取得
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .select('skill_id')
      .eq('user_id', userid);

    if (userSkillError || !userSkillData || userSkillData.length === 0) {
      toast({
        title: 'Skill IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user skills:', userSkillError);
      setLoading(false);
      return false;
    }
    console.log('step2', userSkillData)
    const skill_ids = userSkillData.map((skill) => skill.skill_id);

    // Step 3: skillsテーブルからnameを取得
    const { data: skillsData, error: skillsError } = await supabase
      .from('skills')
      .select('name')
      .eq('id', skill_ids);

    if (skillsError || !skillsData || skillsData.length === 0) {
      toast({
        title: 'skillsが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching skills:', skillsError);
      setLoading(false);
      return false;
    }

    console.log('step3', skillsData)
    const skillsString = skillsData.map(skill => skill.name);

    // Step4:結果をcombinedDataに集約
    const combinedData = userData.map((user) => ({
      ...user, skill_id: skill_ids, skills: skillsString,
    }));

    console.log('step4', combinedData)
    setCardData(combinedData);
    toast({
      title: 'データを取得しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    setLoading(false);
    return true;  // データ取得に成功した場合はtrueを返す
  };

  //DBへの新規データ登録
  // Step 1: usersへの登録
  const insertDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    const { data: userData, error: userError } = await supabase
      .from('users')
      .insert([
        {
          user_id: card.user_id,
          name: card.name,
          description: card.description,
          github_id: card.github_id,
          qiita_id: card.qiita_id,
          x_id: card.x_id,
        },
      ])
      .select();
    if (userError || !userData) {
      console.error('Error insert data:', userError);
      toast({
        title: 'ユーザ登録が失敗しました',
        description: `${userError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      setLoading(false);
      return false;
    }
    console.log('insertDb step1', userData, userError)
    // Step 2: user_skillテーブルへの登録
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .insert([
        {
          user_id: card.user_id,
          skill_id: card.skill_id,
        },
      ])
      .select();
    if (userSkillError || !userSkillData) {
      console.error('Error insert data:', userSkillError);
      toast({
        title: 'ユーザ登録が失敗しました',
        description: `${userSkillError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      setLoading(false);
      return false;
    }
    setLoading(false);
    toast({
      title: 'ユーザ登録に成功しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    console.log('iinsertDb step2', userSkillData);
    return true;
  }

  //登録ユーザの重複チェック
  const userCheckDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', card.user_id);

    if (userData && userData.length > 0) {
      toast({
        title: '既にIDが使われています',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('既にIDが使われています:', userError, userData);
      setLoading(false);
      return false;
    }

    setLoading(false);
    return true;
  }

  //DBの更新処理
  const updateDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    //step1:usersテーブルの更新
    const { data: userData, error: userError } = await supabase
      .from('users')
      .update({
        name: card.name,
        description: card.description,
        github_id: card.github_id,
        qiita_id: card.qiita_id,
        x_id: card.x_id,
      })
      .eq('user_id', card.user_id)
      .select();
    if (userError || !userData || userData.length === 0) {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user data:', userError);
      setLoading(false);
      return false;
    }
    console.log('dbUpdate step1', userData)

    // Step 2: user_skillテーブルへの登録、user_skillテーブルは値が変わってないデータをupdateするとエラーになるため、値が同一かどうかをチェック
    // Step 2-1: 現在のskill_idを取得
    const { data: currentUserSkillData, error: currentUserSkillError } = await supabase
      .from('user_skill')
      .select('skill_id')
      .eq('user_id', card.user_id);
    console.log('dbUpdate step2-1', currentUserSkillData)

    if (currentUserSkillError || !currentUserSkillData) {
      toast({
        title: '現在の Skill が取得できません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching current user skills:', currentUserSkillError);
      setLoading(false);
      return false;
    }

    const currentSkillIds = currentUserSkillData.map((skill) => skill.skill_id);
    console.log('dbUpdate step2-1比較', currentUserSkillData, currentSkillIds, card.skill_id)
    const newSkillIds = Array.isArray(card.skill_id) ? card.skill_id : [card.skill_id]; // 配列でない場合に配列化

    // 現在の skill_id 配列と新しい skill_id 配列が異なる場合のみ更新
    const isDifferent = newSkillIds.some((id: number) => !currentSkillIds.includes(id));

    if (isDifferent) {
      const { data: userSkillData, error: userSkillError } = await supabase
        .from('user_skill')
        .update({
          skill_id: card.skill_id,
        })
        .eq('user_id', card.user_id)
        .select();

      if (userSkillError || !userSkillData || userSkillData.length === 0) {
        toast({
          title: 'Skill が見つかりません',
          position: 'top',
          status: 'error',
          duration: 2000,
          isClosable: true,
        })
        console.error('Error updating user skills:', userSkillError);
        setLoading(false);
        return false;
      }
      console.log('dbUpdate step2', userSkillData);
    } else {
      console.log('Skill ID は変更されていないため、更新しません');
    }

    setLoading(false);
    toast({
      title: '更新が完了しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    return true;
  }

  //DBの削除処理追加
  const deleteDb = async (userid: string): Promise<boolean> => {//仮設置
    return true;
  }

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUserid(e.target.value)
  }

  const handleSearch = async () => {
    if (userid) {
      const success = await selectDb();
      if (success) {
        navigate(`/card/${userid}`);
      }
    } else {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
    }
  };

  return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb} deleteDb={deleteDb} />//Cardsに渡すProps、deleteDb追加
        } />
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
      </Routes>
    </>
  )
}

export default App

7.2 Card.tsxの修正

続いて、Cardコンポーネントを修正していきます。DeleteコンポーネントはCardsコンポーネントにモーダルの形で呼び出される為、Deleteコンポーネントに渡すPropsはCardsコンポーネント経由で渡されます。

下記は、Card.tsxのコード全文です。

// /src/components/Cards.tsx
import { Link, useNavigate } from "react-router-dom";
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Icon, Text } from "@chakra-ui/react";
import { CardData } from "../cardData";
import { FaGithub } from "react-icons/fa";
import { SiQiita } from "react-icons/si";
import { FaXTwitter } from "react-icons/fa6";
import Edit from "./Edit";
import Delete from "./Delete";//Deleteコンポーネントのインポート

type CardsProps = {
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    loading: boolean;
    setLoading: React.Dispatch<React.SetStateAction<boolean>>;
    updateDb: (card: CardData) => Promise<boolean>;
    selectDb: (user_id: string) => Promise<boolean>;
    deleteDb: (user_id: string) => Promise<boolean>;//Deleteに渡すため追加
}

const Cards: React.FC<CardsProps> = ({ cardData, setCardData, loading, setLoading, updateDb, selectDb, deleteDb }) => { // deleteDb追加
    const navigate = useNavigate();

    return (
        <Flex alignItems='center' justify='center' p={5}>
            <Card maxW='400px'>
                <CardHeader>
                    <Heading size='md' textAlign='center'>Name Card App</Heading>
                </CardHeader>
                <CardBody>
                    {/* ////cardData の配列を map で処理 */}
                    {cardData.map((card, index) => (
                        <div key={index}>
                            <Box borderWidth='1px' borderRadius='lg' p={5}>
                                <Heading size='sm' textTransform='uppercase'>
                                    ID
                                </Heading>
                                <Text pb='2' fontSize='sm'>{card.user_id}</Text>
                                <Heading size='sm' textTransform='uppercase'>
                                    名前
                                </Heading>
                                <Text pb='2' fontSize='sm'>{card.name}</Text>
                                <Heading size='sm' textTransform='uppercase'>
                                    自己紹介
                                </Heading>
                                <Text pb='2'
                                    dangerouslySetInnerHTML={{ __html: card.description }}
                                />
                                <Heading size='sm' textTransform='uppercase'>
                                    好きな技術
                                </Heading>
                                <Text pb='2' fontSize='sm'>{card.skills}</Text>
                                <Flex wrap='nowrap' justifyContent='center' width='100%' >
                                    <Link to={`https://github.com/${card.github_id}`} target='_blank'>
                                        <Icon
                                            as={FaGithub}
                                            fontSize="24px"
                                            margin={1}
                                            _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                        />
                                    </Link>
                                    <Link to={`https://qiita.com/${card.qiita_id}`} target='_blank'>
                                        <Icon
                                            as={SiQiita}
                                            fontSize="24px"
                                            margin={1}
                                            _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                        />
                                    </Link>
                                    <Link to={`https://x.com/${card.x_id}`} target='_blank'>
                                        <Icon
                                            as={FaXTwitter}
                                            fontSize="24px"
                                            margin={1}
                                            _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                        />
                                    </Link>
                                </Flex>
                            </Box>
                        </div>
                    ))}
                    <Box mt={4} textAlign='center'>
                        <Edit loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} updateDb={updateDb} selectDb={selectDb} />
                        {/*削除ここから
                        <Button
                            colorScheme='orange'
                            variant='outline'
                            mr='3'>
                            削除</Button>
                            削除ここまで*/}
                        <Delete loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} deleteDb={deleteDb}//Deleteコンポーネント読込追加
                        />
                        <Button
                            colorScheme='gray'
                            variant='outline'
                            onClick={() => {
                                setCardData([]);
                                navigate('/');
                            }
                            }>戻る</Button>
                    </Box>
                </CardBody>
            </Card>
        </Flex>
    )
}

export default Cards;

解説していきます。
まず、冒頭のインポート及び型定義、コンポーネント定義の箇所です。

// /src/components/Cards.tsx
import { Link, useNavigate } from "react-router-dom";
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Icon, Text } from "@chakra-ui/react";
import { CardData } from "../cardData";
import { FaGithub } from "react-icons/fa";
import { SiQiita } from "react-icons/si";
import { FaXTwitter } from "react-icons/fa6";
import Edit from "./Edit";
import Delete from "./Delete";//Deleteコンポーネントのインポート追加

type CardsProps = {
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    loading: boolean;
    setLoading: React.Dispatch<React.SetStateAction<boolean>>;
    updateDb: (card: CardData) => Promise<boolean>;
    selectDb: (user_id: string) => Promise<boolean>;
    deleteDb: (user_id: string) => Promise<boolean>;//Deleteに渡すため追加
}

const Cards: React.FC<CardsProps> = ({ cardData, setCardData, loading, setLoading, updateDb, selectDb, deleteDb }) => { // deleteDb追加
    const navigate = useNavigate();

インポートの箇所は、新たにDeleteコンポーネントのインポートを追加しています。
型定義ですが、Cards経由でDeleteにPropsを渡しますので、その為の型定義、deleteDbを追加しています。
続いて、コンポーネントの定義の箇所は、受け取るDelete用のPropsとして、型定義と同様、deleteDbを追加しています。

続いて、JSX箇所です。

// /src/components/Cards.tsx

return (
    <Flex alignItems='center' justify='center' p={5}>
        <Card maxW='400px'>
            <CardHeader>
                <Heading size='md' textAlign='center'>Name Card App</Heading>
            </CardHeader>
            <CardBody>
                {/* ////cardData の配列を map で処理 */}
                {cardData.map((card, index) => (
                    <div key={index}>
                        <Box borderWidth='1px' borderRadius='lg' p={5}>
                            <Heading size='sm' textTransform='uppercase'>
                                ID
                            </Heading>
                            <Text pb='2' fontSize='sm'>{card.user_id}</Text>
                            <Heading size='sm' textTransform='uppercase'>
                                名前
                            </Heading>
                            <Text pb='2' fontSize='sm'>{card.name}</Text>
                            <Heading size='sm' textTransform='uppercase'>
                                自己紹介
                            </Heading>
                            <Text pb='2'
                                dangerouslySetInnerHTML={{ __html: card.description }}
                            />
                            <Heading size='sm' textTransform='uppercase'>
                                好きな技術
                            </Heading>
                            <Text pb='2' fontSize='sm'>{card.skills}</Text>
                            <Flex wrap='nowrap' justifyContent='center' width='100%' >
                                <Link to={`https://github.com/${card.github_id}`} target='_blank'>
                                    <Icon
                                        as={FaGithub}
                                        fontSize="24px"
                                        margin={1}
                                        _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                    />
                                </Link>
                                <Link to={`https://qiita.com/${card.qiita_id}`} target='_blank'>
                                    <Icon
                                        as={SiQiita}
                                        fontSize="24px"
                                        margin={1}
                                        _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                    />
                                </Link>
                                <Link to={`https://x.com/${card.x_id}`} target='_blank'>
                                    <Icon
                                        as={FaXTwitter}
                                        fontSize="24px"
                                        margin={1}
                                        _hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
                                    />
                                </Link>
                            </Flex>
                        </Box>
                    </div>
                ))}
                <Box mt={4} textAlign='center'>
                    <Edit loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} updateDb={updateDb} selectDb={selectDb} />
                    {/*削除ここから
                    <Button
                        colorScheme='orange'
                        variant='outline'
                        mr='3'>
                        削除</Button>
                        削除ここまで*/}
                    {/*追加ここから*/}
                    <Delete loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} deleteDb={deleteDb}//Deleteコンポーネント読込追加
                    />
                    {/*追加ここまで*/}
                    <Button
                        colorScheme='gray'
                        variant='outline'
                        onClick={() => {
                            setCardData([]);
                            navigate('/');
                        }
                        }>戻る</Button>
                </Box>
            </CardBody>
        </Card>
    </Flex>
)

JSXの箇所は、Editと同じような変更です。
これまで「削除」ボタンとして、<Button>を配置していた箇所を、Deleteコンポーネントの配置に変更します。ボタン部分を削除して、新たに<Delete …/>を追加します。
<Deleteに渡すPropsは、冒頭の型定義と同様、
loading={loading} setLoading={setLoading} cardData={cardData} setCardData={setCardData} deleteDb={deleteDb} となります。

7.3 Delete.tsxの作成

Delete.tsxにコードを具体的に記載していきます。
以下は、Delete.tsxのコード全文です。

// /src/components/Delete.tsx
import { useRef } from "react";
import { Box, Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure, useToast } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { CardData } from "../cardData";

type DeleteProps = {
    loading: boolean;
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    setLoading: React.Dispatch<React.SetStateAction<boolean>>;
    deleteDb: (user_id: string) => Promise<boolean>;
}


const Delete: React.FC<DeleteProps> = ({ loading, cardData, setCardData, deleteDb }) => {
    const { isOpen, onOpen, onClose } = useDisclosure()
    const initialRef = useRef(null)
    const finalRef = useRef(null)
    const navigate = useNavigate();
    const toast = useToast()

    const handleDelete = async () => {
        const isDeleted = await deleteDb(cardData[0].user_id)
        if (isDeleted) {
            setCardData([])
            onClose()
            navigate('/')
        } else {
            toast({
                title: '削除に失敗しました',
                position: 'top',
                status: 'error',
                duration: 2000,
                isClosable: true,
            })
        }
    }

    return (
        <>
            <Button //Cards上に表示されるボタン部部分
                isLoading={loading}
                loadingText='Loading'
                colorScheme='orange'
                variant='outline'
                spinnerPlacement='start'
                mr='3'
                onClick={onOpen}>
                削除</Button>

            <Modal //ここからモーダル箇所
                initialFocusRef={initialRef}
                finalFocusRef={finalRef}
                isOpen={isOpen}
                onClose={onClose}
            >
                <ModalOverlay />
                <ModalContent>
                    <ModalHeader>データ削除</ModalHeader>
                    <ModalCloseButton />
                    <ModalBody pb={6}>
                        <Box>
                            データを削除します
                        </Box>
                    </ModalBody>
                    <ModalFooter>
                        <Button
                            isLoading={loading}
                            loadingText='Loading'
                            colorScheme='orange'
                            spinnerPlacement='start'
                            mr={3}
                            onClick={handleDelete}
                        >
                            削除
                        </Button>
                        <Button onClick={onClose} variant='outline'>Cancel</Button>
                    </ModalFooter>
                </ModalContent>
            </Modal>
        </>
    )
}

export default Delete;

それぞれ見ていきます。
まず冒頭のインポート、コンポーネント定義の箇所です。

// /src/components/Delete.tsx
import { useRef } from "react";//useRefのインポート、Chakra UIのModalで使用
import { Box, Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure, useToast } from "@chakra-ui/react";//Chakra UIのインポート
import { useNavigate } from "react-router-dom";//React Router, useNavigateインポート
import { CardData } from "../cardData";//CardData型定義のインポート

type DeleteProps = {//Propsの型定義
    loading: boolean;
    cardData: CardData[];
    setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
    setLoading: React.Dispatch<React.SetStateAction<boolean>>;
    deleteDb: (user_id: string) => Promise<boolean>;
}


const Delete: React.FC<DeleteProps> = ({ loading, cardData, setCardData, deleteDb }) => {
    const { isOpen, onOpen, onClose } = useDisclosure()//Chakra UIのModal用の定義
    const initialRef = useRef(null)//Chakra UIのModal用の定義
    const finalRef = useRef(null)//Chakra UIのModal用の定義
    const navigate = useNavigate();//useNavigateによる画面遷移の定義
    const toast = useToast()//Chakra UIのToast機能の定義

インポートについては、記載の通りです。React, Chakra UI,React Router, CardDataの型情報のインポートを行っています。ReactのuseRefはChakra UIのModalで使用します。

type設定については、Cards.tsxからloading, cardData, setCardData, deleteDbがPropsとして渡されますので、その定義を行っています。

コンポーネント定義の箇所は、これまでと同様、React.FC<DeleteProps>と型定義し、Cardsより渡されるPropsを設定しています。loading, cardData, setCardData, deleteDb です。
その下の、const設定は、Chakra UIのModal用の定義。その他、useNavigateによる画面遷移のnavigate設定、Chakra UIのToast機能の定義をしています。

次に、「削除」ボタンクリック時の処理についてです。

// /src/components/Delete.tsx

const handleDelete = async () => {//async/awaitによる非同期処理
    const isDeleted = await deleteDb(cardData[0].user_id)//cardData[0]のuser_idをキーに、deleteDbを実行
    if (isDeleted) {//処理が成功すれば
        setCardData([])//cardDataを初期化
        onClose()//モーダルをクローズ
        navigate('/')// '/'に遷移
    } else {
        toast({//エラー時は、Chakra UIのToastでエラー表示
            title: '削除に失敗しました',
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
    }
}

「削除」ボタンをクリックすると実行されるのが、ここで定義している、handleDeleteです。
これまでの各コンポーネントと同じように、async/awaitによる非同期処理で、DBデータの削除処理(deleteDb)を実行しています。その際に対象となるのは、cardData配列の最初のレコード(と言うより、最初のレコードしかありません)のuser_idです。このuser_idに紐付くデータを削除します。

処理が成功すれば、cardDataを初期化、モーダルをクローズの上、ルート(/)のHome画面に遷移しています。エラー時は、Chakra UIのToastでエラー表示しています。

最後のJSX箇所です。

// /src/components/Delete.tsx

return (
    <>
        <Button //Cards上に表示されるボタン部部分
            isLoading={loading}
            loadingText='Loading'
            colorScheme='orange'
            variant='outline'
            spinnerPlacement='start'
            mr='3'
            onClick={onOpen}>
            削除</Button>

        <Modal //ここからモーダル箇所
            initialFocusRef={initialRef}
            finalFocusRef={finalRef}
            isOpen={isOpen}
            onClose={onClose}
        >
            <ModalOverlay />
            <ModalContent>
                <ModalHeader>データ削除</ModalHeader>
                <ModalCloseButton />
                <ModalBody pb={6}>
                    <Box>
                        データを削除します
                    </Box>
                </ModalBody>
                <ModalFooter>
                    <Button
                        isLoading={loading}
                        loadingText='Loading'
                        colorScheme='orange'
                        spinnerPlacement='start'
                        mr={3}
                        onClick={handleDelete}//ボタンクリック時、handleDeleteを実行
                    >
                        削除
                    </Button>
                    <Button onClick={onClose} variant='outline'//ボタンクリック時、モーダルクローズ
                    >Cancel</Button>
                </ModalFooter>
            </ModalContent>
        </Modal>
    </>
)

冒頭の<Button>の箇所は、Cardsコンポーネントに表示される、「削除」ボタンの箇所です。クリックすると、モーダルがオープンします。
その下の<Modal>の箇所からがモーダルで表示される内容です。

モーダルの中身は、「削除」ボタンと「Cancel」ボタンです。「削除」ボタンをクリックすると、先に定義した、handleDeleteが実行され、DBデータの削除処理を行います。「Cancel」ボタンをクリックした場合は、単純にモーダルをクローズします。

ここまでで、Cardsコンポーネントから「削除」ボタンをクリックすると下図のような画面が表示されます。

7.4 データ削除機能の実装

それでは、App.tsxに仮設置してる、deleteDbの処理を作成していきます。
以下、deleteDbDbの全体です。

// /src/components/Delete.tsx

//DBの削除処理
const deleteDb = async (userid: string): Promise<boolean> => {//async/awaitによる非同期処理
    setLoading(true);//ローディングをローディング中にセット

    // Step 1: usersテーブルからuser_idを削除
    const { error: userError } = await supabase//Supabaseのdelete(データ削除)処理を実行
        .from('users')//usersテーブルに対して
        .delete()
        .eq('user_id', userid);//削除対象のuser_id、userid
    if (userError) {//エラーありの場合は、
        toast({//Chakra UIのトーストにてエラーメッセージ表示
            title: '削除が失敗しました',
            description: `${userError}`,
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        console.error('Error delete data:', userError);
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }
    // Step 2: user_skillテーブルからuser_idを削除
    const { error: userSkillError } = await supabase//Supabaseのdelete(データ削除)処理を実行
        .from('user_skill')//user_skillテーブルに対して
        .delete()
        .eq('user_id', userid);//削除対象のuser_id、userid
    if (userError) {//エラーありの場合は、
        toast({//Chakra UIのトーストにてエラーメッセージ表示
            title: '削除が失敗しました',
            description: `${userSkillError}`,
            position: 'top',
            status: 'error',
            duration: 2000,
            isClosable: true,
        })
        console.error('Error delete data:', userSkillError);
        setLoading(false);//ローティング状態を解除
        return false;//falseをリターン
    }
    //全て正常処理されれば
    setLoading(false);//ローティング状態を解除
        toast({//Chakra UIのトーストにて成功メッセージ表示
        title: 'データを削除しました',
        position: 'top',
        status: 'success',
        duration: 2000,
        isClosable: true,
    })
    return true;//trueをリターン
}

これまでの、データ取得、登録、更新と同じような流れでの処理です。Stepを分けて、usersテーブルの処理、user_skillデーブルの処理を行っています。

  • 非同期通信、async/awaitで処理開始、ローディング状態をローディング中にセット
  • Step1:usersテーブルの削除
    • usersテーブルに対して、user_idがuseridの箇所のデータを削除
    • エラー発生の場合、エラー処理(Chkra UIのトースト機能)、ローディング解除、falseリターン
    • 正常終了であればStep2に推移
  • Step2:user_skillテーブルの削除
    • user_skillテーブルに対して、user_idがuseridの箇所のデータを削除
    • エラー発生の場合、エラー処理(Chkra UIのトースト機能)、ローディング解除、falseリターン
    • 正常終了であれば次処理に推移
  • Step1、2、正常終了すれば、ローディング解除、Chakra UIのトーストにて成功メッセージ表示、trueをリターン

実行すると以下のような画面推移となります。

これで、名刺アプリは一通り完了です。
最終的なApp.tsxのコード全体は以下となります。

// /src/App.tsx

import { useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom"
import { useToast } from "@chakra-ui/react";
import Home from "./components/Home"
import Cards from "./components/Cards";
import { CardData } from "./cardData";
import { supabase } from "./supabaseClient";
import Register from "./components/Register";

function App() {
  const [userid, setUserid] = useState<string>();
  const [cardData, setCardData] = useState<CardData[]>([]);
  const [newCard, setNewCard] = useState<CardData>({
    user_id: '',
    name: '',
    description: '',
    github_id: '',
    qiita_id: '',
    x_id: '',
    skill_id: 0,
    skills: '',
  });
  const [loading, setLoading] = useState<boolean>(false);
  const toast = useToast()
  const navigate = useNavigate()

  // Supabaseからデータを取得する関数
  const selectDb = async (): Promise<boolean> => {
    setLoading(true);

    // Step 1: usersテーブルからuser_idを取得
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', userid);

    if (userError || !userData || userData.length === 0) {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user data:', userError);
      setLoading(false);
      return false;
    }
    console.log('step1', userData)

    // Step 2: user_skillテーブルから該当するskill_idを取得
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .select('skill_id')
      .eq('user_id', userid);

    if (userSkillError || !userSkillData || userSkillData.length === 0) {
      toast({
        title: 'Skill IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user skills:', userSkillError);
      setLoading(false);
      return false;
    }
    console.log('step2', userSkillData)
    const skill_ids = userSkillData.map((skill) => skill.skill_id);

    // Step 3: skillsテーブルからnameを取得
    const { data: skillsData, error: skillsError } = await supabase
      .from('skills')
      .select('name')
      .eq('id', skill_ids);

    if (skillsError || !skillsData || skillsData.length === 0) {
      toast({
        title: 'skillsが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching skills:', skillsError);
      setLoading(false);
      return false;
    }

    console.log('step3', skillsData)
    const skillsString = skillsData.map(skill => skill.name);

    // Step4:結果をcombinedDataに集約
    const combinedData = userData.map((user) => ({
      ...user, skill_id: skill_ids, skills: skillsString,
    }));

    console.log('step4', combinedData)
    setCardData(combinedData);
    toast({
      title: 'データを取得しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    setLoading(false);
    return true;  // データ取得に成功した場合はtrueを返す
  };

  //DBへの新規データ登録
  // Step 1: usersへの登録
  const insertDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    const { data: userData, error: userError } = await supabase
      .from('users')
      .insert([
        {
          user_id: card.user_id,
          name: card.name,
          description: card.description,
          github_id: card.github_id,
          qiita_id: card.qiita_id,
          x_id: card.x_id,
        },
      ])
      .select();
    if (userError || !userData) {
      console.error('Error insert data:', userError);
      toast({
        title: 'ユーザ登録が失敗しました',
        description: `${userError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      setLoading(false);
      return false;
    }
    console.log('insertDb step1', userData, userError)
    // Step 2: user_skillテーブルへの登録
    const { data: userSkillData, error: userSkillError } = await supabase
      .from('user_skill')
      .insert([
        {
          user_id: card.user_id,
          skill_id: card.skill_id,
        },
      ])
      .select();
    if (userSkillError || !userSkillData) {
      console.error('Error insert data:', userSkillError);
      toast({
        title: 'ユーザ登録が失敗しました',
        description: `${userSkillError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      setLoading(false);
      return false;
    }
    setLoading(false);
    toast({
      title: 'ユーザ登録に成功しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    console.log('iinsertDb step2', userSkillData);
    return true;
  }

  //登録ユーザの重複チェック
  const userCheckDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    const { data: userData, error: userError } = await supabase
      .from('users')
      .select('*')
      .eq('user_id', card.user_id);

    if (userData && userData.length > 0) {
      toast({
        title: '既にIDが使われています',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('既にIDが使われています:', userError, userData);
      setLoading(false);
      return false;
    }

    setLoading(false);
    return true;
  }

  //DBの更新処理
  const updateDb = async (card: CardData): Promise<boolean> => {
    setLoading(true);
    //step1:usersテーブルの更新
    const { data: userData, error: userError } = await supabase
      .from('users')
      .update({
        name: card.name,
        description: card.description,
        github_id: card.github_id,
        qiita_id: card.qiita_id,
        x_id: card.x_id,
      })
      .eq('user_id', card.user_id)
      .select();
    if (userError || !userData || userData.length === 0) {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching user data:', userError);
      setLoading(false);
      return false;
    }
    console.log('dbUpdate step1', userData)

    // Step 2: user_skillテーブルへの登録、user_skillテーブルは値が変わってないデータをupdateするとエラーになるため、値が同一かどうかをチェック
    // Step 2-1: 現在のskill_idを取得
    const { data: currentUserSkillData, error: currentUserSkillError } = await supabase
      .from('user_skill')
      .select('skill_id')
      .eq('user_id', card.user_id);
    console.log('dbUpdate step2-1', currentUserSkillData)

    if (currentUserSkillError || !currentUserSkillData) {
      toast({
        title: '現在の Skill が取得できません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error fetching current user skills:', currentUserSkillError);
      setLoading(false);
      return false;
    }

    const currentSkillIds = currentUserSkillData.map((skill) => skill.skill_id);
    console.log('dbUpdate step2-1比較', currentUserSkillData, currentSkillIds, card.skill_id)
    const newSkillIds = Array.isArray(card.skill_id) ? card.skill_id : [card.skill_id]; // 配列でない場合に配列化

    // 現在の skill_id 配列と新しい skill_id 配列が異なる場合のみ更新
    const isDifferent = newSkillIds.some((id: number) => !currentSkillIds.includes(id));

    if (isDifferent) {
      const { data: userSkillData, error: userSkillError } = await supabase
        .from('user_skill')
        .update({
          skill_id: card.skill_id,
        })
        .eq('user_id', card.user_id)
        .select();

      if (userSkillError || !userSkillData || userSkillData.length === 0) {
        toast({
          title: 'Skill が見つかりません',
          position: 'top',
          status: 'error',
          duration: 2000,
          isClosable: true,
        })
        console.error('Error updating user skills:', userSkillError);
        setLoading(false);
        return false;
      }
      console.log('dbUpdate step2', userSkillData);
    } else {
      console.log('Skill ID は変更されていないため、更新しません');
    }

    setLoading(false);
    toast({
      title: '更新が完了しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    return true;
  }

  //DBの削除処理
  const deleteDb = async (userid: string): Promise<boolean> => {
    setLoading(true);

    // Step 1: usersテーブルからuser_idを削除
    const { error: userError } = await supabase
      .from('users')
      .delete()
      .eq('user_id', userid);
    if (userError) {
      toast({
        title: '削除が失敗しました',
        description: `${userError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error delete data:', userError);
      setLoading(false);
      return false;
    }
    // Step 2: user_skillテーブルからuser_idを削除
    const { error: userSkillError } = await supabase
      .from('user_skill')
      .delete()
      .eq('user_id', userid);
    if (userError) {
      toast({
        title: '削除が失敗しました',
        description: `${userSkillError}`,
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
      console.error('Error delete data:', userSkillError);
      setLoading(false);
      return false;
    }
    setLoading(false);
    toast({
      title: 'データを削除しました',
      position: 'top',
      status: 'success',
      duration: 2000,
      isClosable: true,
    })
    return true;
  }

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUserid(e.target.value)
  }

  const handleSearch = async () => {
    if (userid) {
      const success = await selectDb();
      if (success) {
        navigate(`/card/${userid}`);
      }
    } else {
      toast({
        title: 'IDが見つかりません',
        position: 'top',
        status: 'error',
        duration: 2000,
        isClosable: true,
      })
    }
  };

  return (
    <>
      <Routes>
        <Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />
        <Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} loading={loading} setLoading={setLoading} updateDb={updateDb} selectDb={selectDb} deleteDb={deleteDb} />
        } />
        <Route path='/card/register' element={<Register loading={loading} newCard={newCard} setNewCard={setNewCard} setUserid={setUserid} insertDb={insertDb} userCheckDb={userCheckDb} />} />
      </Routes>
    </>
  )
}

export default App

8. GitHub、Firebase連携

一通り、コード作成が完了しましたので、リポジトリ作成とGitHubへのコミット(push)、そしてFirebaseとGitHub連携を行い、GitHubプッシュ時にFirebaseに自動デプロイされるようにします。
この一連の作業は、過去投稿した下記記事に詳細を記載してますので、ご参照ください。プロジェクト名等を読み替えていただくだけです。

なお、注意点ですが、Firebaseにデプロイした場合、React Routerは、リロードした場合にパス設定がうまく動かず、404エラーとなります(/homeでリロードした場合等)。
これを回避するためには、下記記事が参考になります。

具体的にはプロジェクト直下にある、firebase.json に以下内容を追加します。

// /firebase.json
{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
    /*追加ここから*/
    ,
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
    /*追加ここまで*/
  }
}
上記は、TypeScriptのコメントアウト形式で追加内容を明示してますが、jsonファイルはコメントは記載出来ないのでご注意ください(コメントの箇所は省いてください)。

その他の注意点ですが、

  • リポジトリは、Privateをおすすめします。.envの中にSupabase接続の機密情報が含まれるためです。
  • .gitgnoreファイルにGitHub同期除外対象として、.envを定義する事も可能ですが、この場合は、GitHubでのビルド、Firebaseでのデプロイはうまく行きませんので(.env情報が無いため)、ご注意ください。
    一見、ビルド及びデプロイは成功しますが、アプリが動きません(Supabase接続情報が見つからないとエラーになります)

Firebaseでデプロイして、ドメインの箇所からリリースされたアプリケーションを確認してみましょう。アプリが表示されれば公開完了です!

名刺アプリこれで完了です。

React チュートリアル:名刺アプリシリーズ

No responses yet

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

AD




TWITTER


アーカイブ
OTHER ISSUE
PVアクセスランキング にほんブログ村