Reactのチュートリアル記事です。
今回の記事も、以下の記事をモチーフに作成した名刺アプリのチュートリアルです。
こちらの「4. 名刺アプリの開発」をベースに進めてます。元記事では、データの格納にBaaS(Backend as a Service)であるSupabaseを利用したものとなってますので、同様にSupabaseを活用して開発したいと思います。なお、参考記事ではCI/CDの為のテスト実装まで触れてますが、本記事ではテストまでは言及していません。機能面に焦点を当てています。なお、記事が長くなってしまったので、前編・後編と2回に分けてお届けします。
今回は前編となります。前編ではSupabaseの設定、環境構築、SupabaseのDBからテーブル情報の取得までを記載しています。後編では、新規データ登録、データ編集、データ削除機能の実装を解説します。
後編も公開しました!
目次
はじめに
本記事は、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と連携した自動デプロイ 等
1. Supabase環境の準備
まずは、Supabaseのアカウントを作成します。Supabaseのサイトに行き、Sign Upをします。
https://supabase.com/
GitHubアカウントとの連携も出来ます。
アカウント作成後、NewProjectで新しいプロジェクトを作成します。プロジェクト名はお好きなものをつけてください。Regionの選択はデフォルトで結構です(違うものでもいいかと思います)。
1.1 テーブルの作成
作成したプロジェクトを選択の上、左側にhover時表示されるメニューから「Table Editor」を選択します。
「Create a new table]を選択します。
「users」「user_skill」「skills」の3つのテーブルを作成します。
Descriptionは任意で
Enable Row Level Security (RLS)Recommended は、デフォルトのONのままで
Enable Realtime は不要です。
Columnsはそれぞれ以下内容で設定します。
usersテーブル
Name | Type | Dafault Value | Primary | Extra Options |
---|---|---|---|---|
user_id | varchar | – | ✓ | |
name | varchar | – | 選択なし | |
description | text | – | 選択なし | |
github_id | varchar | – | Is Nullable | |
qiita_id | varchar | – | Is Nullable | |
x_id | varchar | – | Is Nullable |
user_skillテーブル
Name | Type | Dafault Value | Primary | Extra Options |
---|---|---|---|---|
id | int8 | – | ✓ | Is Identity |
user_id | varchar | – | 選択なし | |
skill_id | int8 | – | 選択なし |
skillsテーブル
Name | Type | Dafault Value | Primary | Extra Options |
---|---|---|---|---|
id | int8 | – | ✓ | Is Identity |
name | varchar | – | 選択なし |
一旦、Foreign keysは不要です(これは外部キー連携の機能ですが、機会があれば、利用してみたいと思います)。
1.2 テーブルデータの作成
次に作成したtableにデータを作成しておきます。
左メニューのTable Editorから作成したテーブルを選択し、insert → insert lowをクリックします。
右側にデータの内容を入力するエリアが表示されますので、データを入力します。ひとまず、初期データとして下記のデータをセットします。
usersテーブル
カラム名 | 設定する値 |
---|---|
user_id | sample_id |
name | テスト太郎 |
description | <h1>テスト太郎の自己紹介</h1> |
github_id | あなたのgithubのID(あれば) |
qiita_id | あなたのQiitaのID(あれば) |
x_id | あなたのXのID(あれば) |
user_skillテーブル
カラム名 | 設定する値 |
---|---|
id | 自動セットなので不要 |
user_id | sample_id |
skill_id | 1 |
skillsテーブル
skillsデーブルについては以下の3つのデータを作成しておきます。
id | Name |
---|---|
1(自動でセットされる) | React |
2(自動でセットされる) | TypeScript |
3(自動でセットされる) | Github |
それぞれのテーブル内容はSupabase上で以下のようになります。
usersテーブル
user_skillテーブル
skillsテーブル
1.3 ポリシーの作成
続いて作成したテーブルに対して、ポリシー作成を行います。これはテーブルのデータ操作(SELECT,INSERT,UPDATE,DELETE)に対する権限を設定するものです。
作成テーブルの右側にある、…アイコンの箇所をクリックすると表示されるメニューから、View Policies を選択します。
表示される画面の右側の、Create Policyをクリックします。
以下画面のように設定していきます。
- Policy Name:任意のポリシー名を入力します。
- Polciy Command:ALLを選択、全ての操作に共通のポリシーとしてます。
- Target Roles:anonを選択
- Use options above to edit:7行目に、tureと記載
- Use check expression:チェックしない
この内容で、画面下にある、Save policyをクリックしてポリシーを保存します。
上記はusersテーブルの例ですが、他のuser_skillテーブル、skillsテーブルも同じ手順でポリシーを作成します。
これでSupabase側の設定は完了です。
2. 環境構築
それでは開発環境の構築をしていきます。JavaScript実行環境のnode.jsとパッケージマネージャのnpmは既にある前提とします。
(ない場合は、こちらの記事を参照頂ければと思います。)
各画面遷移には、React Routerを利用します。React Routerは、複数のページを持つReactアプリケーションを構築する際に利用されるライブラリです。React Routerを使うことで、ユーザーの操作に応じて表示内容を変更したり、URLにパラメータやクエリを含めて表示内容を変更したりすることができます。
2.1 アプリ構造
今回作成するアプリの構造は下図のイメージです。
- 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”でセットします。
2.2 環境準備
まずは、Viteを利用してReactプロジェクトを作成します。ターミナルでnpm create vite@latest を入力しプロジェクト作成を実行します。プロジェクト名は「name-cards」で他の選択肢は下記の通りです。
npm create vite@latest
✔ Project name: … name-cards
✔ Select a framework: › React
✔ Select a variant: › TypeScript
これでReactの初期環境構築は完了です。
続けて出てくる内容に従って、環境起動を行います。
cd name-cards
npm i
npm run dev
npm i は、プロジェクト作成時に生成された、プロジェクトのライブラリの依存環境が記載されたpackage.jsonに明示されているすべてのパッケージをインストールします。
npm run devは、開発用のサーバの起動を行います。これにより、ブラウザで開発プロジェクトの実行結果を確認出来ます。通常開発サーバは、http://localhost:5173/
で起動されます。
localhost:5173にアクセスすると以下の画面が表示されます
次に、Chakra UIのインストールです。
Chakra UIのサイトの「Get Started」に「Installation」の記載がありますので、それに従ってターミナルでコマンドを投入してください。
サーバを起動したターミナルで「ctrl-c」を入力し一度サーバ停止するか、別のターミナルを開いてlearning-recordsディレクトリに移動してインストールします
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
続いてReact IconsとReact Routerをインストールします。
npm i react-icons react-router-dom
そして、Supabaseのライブラリをインストールします。
npm i @supabase/supabase-js
一度開発サーバーを落とした場合は、npm run dev コマンドで、再度、開発サーバを起動します。以降インストールする際に開発サーバーを停止してインストールコマンドを実行した場合は、同様に操作します。(開発サーバー起動したままで別のターミナルウィドウからインストールコマンドを実行することも可能です)
次にVSCode等のエディタでコードを記載していきます。VSCode等のエディタでlearning-recordsディレクトリを開きます。なお、ローカルで開発される方は、拡張性の高さ、使い勝手の良さ、作業効率などで、VSCodeが圧倒的にお薦めです。
プロジェクトルートのsrcフォルダ内、App.tsxを開き、以下のようにコードを記載します。
以下は、App.tsxの初期内容です。これらは不要ですので、一旦全て削除します。
// /src/App.tsx
//↓全て削除します
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
次に以下のように記載します。必要な「枠」だけ残す形です。
// /src/App.tsx
function App() {
return (
<>
</>
)
}
export default App
次にChakra UIのボタンを配置してみます。
// /src/App.tsx
import { Button } from "@chakra-ui/react"//インポート
function App() {
return (
<>
<div>
{/*カラースキームteal、margin 1(=4px)で設定*/}
<Button colorScheme="teal" m='1'>Click me</Button>
</div>
</>
)
}
export default App
上記はボタンの配色は「teal」、margin(m=" "で表現します)で設定しています。なお数値のみの場合は、1=4px(0.25rem)の換算となります。m="5px"等px指定も可能です。
なお、Reactコンポーネントを挿入する場合は(上記で言えば<Button>)、<Button>と入力すれば、VSCodeが自動で関連すると思われるコンポートのインポートを自動で補完してくれます。
下記は参考のGIF動画です。importされていなくても、コンポーネントタグの最後の一文字を削除しまた記載すればimportの候補先が表示され、自動でコードに挿入してくれます。
また、/src/main.tsxについては、Chakra UIのインポートと、<ChakraProvider>タグで囲む記載が必要です。元々記載のある、import ‘./index.css’は削除してください。
// /src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import { ChakraProvider } from '@chakra-ui/react'//インポート
//import './index.css'//削除もしくはコメントアウト
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ChakraProvider>//<ChakraProvider>タグで囲む
<App />
</ChakraProvider>//<ChakraProvider>タグで囲む
</StrictMode>,
)
localhost:5173/ をみるとグリーン(teal)のボタンが表示されます。いい感じです。
2.3 Supabase連携機能の作成
続いて、Supabaseとの連携の為の環境を整備していきます。
Supabaseの環境設定ファイルをプロジェクトルート直下に、また、Supabaseのクライアント機能のファイル、名刺データの型定義ファイルを/src配下に作成します。
- supabaseの環境設定:/.env
- supabaseのクライアント機能:/src/supabaseClient.ts
- 名刺データの型定義:/src/cardData.ts
まずは、.envです。ここには、Supabase接続の為の機密情報を記載しています。
// /.env
VITE_SUPABASE_URL=Supabaseのアクセス先URL
VITE_SUPABASE_ANON_KEY=SupabaseAPIキー
ここで、VITE_SUPABASE_URLとVITE_SUPABASE_ANON_KEYを記載しますが、それぞれの内容は、Supabaseのサイトにて確認します。Supabaseのプロジェクトの設定 > APIから、Project URL及びProject API Keys内のanon keyをコピー(下図参照)し、上記の.envにそれぞれ貼り付けます。
続いて、/src/supabaseClient.tsです。下記コードを記載します。他のコンポーネントからDBデータの参照や更新等の処理を行う際は、この機能を呼び出して利用します。
// /src/supabaseClient.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
そして、型定義の/src/cardData.tsです。下記のコードを記載します。
// /src/cardData.ts
export type CardData = {
user_id: string,
name: string,
description: string,
github_id?: string,
qiita_id?: string,
x_id?: string,
skill_id: number,
skills: string
}
github_id?など、?は付いているものは、オプションの意味となります。これらは、DBテーブル上も必須項目としていないためです。
3 ベースデザインの作成
まずはアプリのベースデザインを作成します。
機能面はこのあと実装していきます。
3.1 Homeコンポーネント
まずは、アプリの最初に表示されるHomeコンポーネントを作成します。
プロジェクトルートの/src配下に、componentsフォルダを新たに作成し、その中に、Home.tsxと言うファイルを作成します。
Home.tsxに以下コードを記載します。まずは、「Home」とだけ表示される内容です。
// /src/components/Home.tsx
const Home = () => {
return (
<div>
<h1>Home</h1>
</div>
)
}
export default Home;
続いて、main.tsxとApp.tsxにReact Routerの設定を行い、Home.tsxが”/”で表示されるようにします。
// /src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
//import './index.css'
import { ChakraProvider } from '@chakra-ui/react'
import { BrowserRouter } from 'react-router-dom' //インポート
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ChakraProvider>
<BrowserRouter>{/*追加 */}
<App />
</BrowserRouter>{/*追加 */}
</ChakraProvider>
</StrictMode>,
)
まず、main.tsxにReact Routerを設定します。冒頭のインポート箇所に、’react-router-dom’をインポートします。また、先程、<ChakraProvider>で囲んだ箇所を更に<BrowserRouter>で囲みます。<BrowserRouter>はルーティングを行うアプリケーション全体をラップするために使用します。
続いて、App.tsxです。
// /src/App.tsx
// import { Button } from "@chakra-ui/react"//削除
import { Route, Routes } from "react-router-dom"//インポート追加
import Home from "./components/Home"//インポート追加
function App() {
return (
<>
{/*追加ここから*/}
<Routes>
<Route path="/" element={<Home />} />
</Routes>
{/*追加ここまで*/}
</>
)
}
export default App
冒頭のインポートの箇所に、”react-router-dom”と、Homeコンポーネントのインポートを追加します。
return以降のJSX※の箇所に、React Routerによる、ルート設定を記載してます。
<Routes>で囲まれた箇所に<Route path=’設定したいパス’ element={読み込むコンポーネント+コンポーネントに渡すデータ(Propsと言います)、もしくはJSXの記載} />のような書き方をします。
Homeはサイトルート”/”で設定してます。
※JSX(JavaScript XML)は、コンポーネント指向のJavaScriptライブラリやフレームワーク(特にReact)で一般的に採用されている、JavaScriptの拡張構文です。JSXを用いると、JavaScriptのコード内にHTMLタグのような構文が埋め込み可能となり、より直感的かつ読みやすい形でUIのコードを表現することができます。それによって、開発者のコーディング体験や開発、デバッグの効率が上がります。
この状態で、http://localhost:5173/にアクセスしてみます。Homeコンポーネントを”/”でルート設定してますので、Homeと言う文字が表示されると思います。
HomeコンポーネントがApp.tsxのルート設定通り、http://localhost:5173/ で表示されることが確認出来ましたので、Home.tsxの内容を具体的に作成します。
// /src/components/Home.tsx
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input } from "@chakra-ui/react";
const Home = () => {
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'
/>
<Box mt={4} >
<Button
colorScheme='blue'
w='100%'>名刺を表示</Button>
</Box>
<Box mt={4} >
<Button
colorScheme='blue'
variant='outline'
w='100%'>新規登録</Button>
</Box>
</CardBody>
</Card>
</Flex>
</>
)
}
export default Home;
Chakra UIのコンポーネントをインポート、利用し、JSX箇所にCard、Box等UIパーツを配置しています。この内容で以下のような画面となります。
今のところは、何も処理の実装はしていないので、ボタン等は表示されるだけです。この後、機能実装をしていきます。
4. テーブルデータ表示機能
まずは、Supabaseのテーブルデータを表示させる機能を作成していきます。HomeコンポーネントでIDを入力すると、IDがマッチする場合は、該当の名刺情報をSupabaseより読み込み、表示させます。この際、URLは、/cards/userid名 のルート設定で表示させるようにします。
名刺データを表示するコンポーネント、Cardsを新たに作成します。また、これに伴い、App.tsx、Home.tsxを変更します。
componentsに、Cards.tsxと言うファイルを作成します。Cards.tsxのコードは後ほど記載します。その前にApp.tsxとHome.tsxを変更していきます。
4.1 App.tsx
まず、App.tsxです。以下は、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";
//インポート追加・変更 ここまで
function App() {
//追加 ここから
const [userid, setUserid] = useState<string>();
const [cardData, setCardData] = useState<CardData[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const toast = useToast()
const navigate = useNavigate()
const selectDb = () => {
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} />} />
{/*ルート変更・追加 ここまで */}
</Routes>
</>
)
}
export default App
解説していきます。
// /src/App.tsx
//インポート追加・変更 ここから
import { useState } from "react";//useState追加
import { Route, Routes, useNavigate } from "react-router-dom"//useNavigate追加
import { useToast } from "@chakra-ui/react";//useToast追加
import Home from "./components/Home"
import Cards from "./components/Cards";//Cardsコンポーネント追加
import { CardData } from "./cardData";//CardData型定義追加
//インポート追加・変更 ここまで
function App() {
const [userid, setUserid] = useState<string>();//ユーザIDのstate
const [cardData, setCardData] = useState<CardData[]>([]);//読み込んだ名刺データを格納するstate
const [loading, setLoading] = useState<boolean>(false);//ローディング状態を管理するstate
const toast = useToast()//Chakura UIのToastの利用
const navigate = useNavigate()//React RouterのNavigate機能を利用
冒頭のインポート箇所ですが、useState、useNavigate、useToastと言った、フック(Hooks)を追加しています。フックはコンポーネントから呼び出し利用可能なReactの様々な機能です。
その他、Cardsコンポーネント及びCardDataの型定義をインポートしています。
function App()以下は、フック関連の定義です。
画面の状態や表示を変更したい値はuseStateを使います。useStateを利用するものとして、userid,cardData,loadingを定義しています。
useridはユーザID情報で文字列(string)、cardDataは名刺データ情報で、先に作成したcardData.tsで定義された型を持つ配列です。loadingはboolean(真偽値)です。
useStateは、const [state, setState] と、値とそれを変更操作するためのset関数の組み合わせで、慣例的にset関数はset+最初大文字の値名とする事が多い(上記でいうと、state, setState)です。
useToastは、Chakra UIのトースト機能を利用するものです。
その下のReact Routerの機能でuseNavigateはページ遷移をする際に使用されます。navigate()の引数に遷移先のパスを渡すことでページを遷移する事ができます。
続いて、操作関連の処理です。
// /src/App.tsx
// Supabaseからデータを取得する関数(まだ中身は記載してません)
const selectDb = () => {
return true;
}
// inputフィールドにユーザIDが入力されたらuseridに入力値をセットする処理
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUserid(e.target.value)
}
// useridをキーにSupabaseのDBからデータを検索し取得する処理。
const handleSearch = async () => {
if (userid) { //useridが存在すれば、selectDb()を実行
const success = await selectDb();
if (success) { //selectDb()が成功すれば、/card/${userid}に移動
navigate(`/card/${userid}`);
}
} else { //selectDb()がエラーであれば、'IDが見つかりません'をトースト表示
toast({
title: 'IDが見つかりません',
position: 'top',
status: 'error',
duration: 2000,//表示時間2秒
isClosable: true,
})
}
冒頭のselectDbは、Supabaseからデータを取得する関数です。今時点はDB処理のコードは記載していません。tureのみを返す内容となっています。後ほど、この機能は記載します。
次の、handleInputChangeは、HomeコンポーネントのユーザID入力フィールドに値が入力されたら、useridのセット関数setUseridでuseridに入力値をセットしています。
このように、値の入力やボタンのクリック等、JSXのイベントをきっかけに動作する処理は慣例的にhandle〜と名付けられるケースが多いです。
一番下の、handleSearchは、Homeコンポーネントで入力されたIDを、ボタンクリックをトリガーにSelectDbに渡しSupabaseのDBにマッチする情報をIDをキーに読出する処理です。
IDが入力されていれば、selectDbを実行。実行結果が成功すれば、useNavigate機能を定義した、navigate(`/card/${userid})で/card/id名に遷移します。
ここで、id名は変数となりますので、${userid}という記載をして変数(定数)useridを挿入しています。ここに入力されたuseridが代入されます。例えば、id名がsample_idであれば、/card/sample_id に遷移します。
また、ここで引数の指定に` `と言う見慣れないもので囲んでいます(JISキー配列でshift-@)。これは、「グレイヴ・アクセント」と言うもので、これで囲むと文字列と変数を結合出来ると言うものです。なお変数は${ }で囲みます。
selectDbの処理がエラー、もしくは、ユーザーIDの入力が空であれば、Chakra UIのトーストでエラー表示します。トーストは下記のようなメッセージ枠を表示する機能です。
最後にJSXの箇所についてです。
// /src/App.tsx
return (
<>
<Routes>
{/*ルート変更・追加 ここから */}
<Route path="/" element={<Home userid={userid || ''} loading={loading} handleInputChange={handleInputChange} handleSearch={handleSearch} />} />//Prps追加
<Route path='/card/:userid' element={<Cards cardData={cardData} setCardData={setCardData} />} />
{/*ルート変更・追加 ここまで */}
</Routes>
</>
)
ルート設定の冒頭のHomeコンポーネントについては、Homeに渡すデータ群(Props)を追記しています。userid, loading, handleInputChange, handleSearchです。なお、useridについては、空の場合もある為、userid={userid || ”} と言う書き方になってます。
続いて、今回作成するCardsコンポーネントのルート設定を追加しています。ルートパスとしては、’/card/:userid’ で、前述の通りuseridの箇所は変数となりますので、:userid とコロンを付与する形になります。これにより変数を反映した可変のパスとなります。
渡すPorpsとして、cardData, setCardData を定義しています。
4.2 Home.tsx
続いて、Home.tsxについてです。
下記に変更後のHome.tsxコード全文を掲載します。
// /src/components/Home.tsx
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input } from "@chakra-ui/react";
//追加ここから
type HomeProps = {
userid: string;
loading: boolean;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSearch: () => Promise<void>;
}
//追加ここまで
const Home: React.FC<HomeProps> = ({ userid, loading, handleInputChange, handleSearch }) => {//変更
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%'>新規登録</Button>
</Box>
</CardBody>
</Card>
</Flex>
</>
)
}
export default Home;
まず冒頭のtypesの定義について説明します。
// /src/components/Home.tsx
//追加ここから
type HomeProps = {
userid: string; //useridの型、string(文字列)
loading: boolean; //loadingの型、boolean(真偽値)
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;//handleInputChangeの型、関数
handleSearch: () => Promise<void>;//handleSearchの型、関数
}
//追加ここまで
App.tsxで記載した通り、App.tsxからHomeにPropsを渡しますので、渡されるPropsの型をHomePropsとして定義しています。userid, とloadingについては記載そのままです。
handleInputChangeと、handleSearchは関数なので、特殊な書き方になっています。VSCodeを利用している場合は、例えばこの関数を定義しているApp.tsx上で関数上にマウスを合わせると型情報がポップアップ表示されます。これをコピーしてそのまま貼り付ければOKです。
次に、Homeコンポーネントの定義箇所です。
// /src/components/Home.tsx
const Home: React.FC<HomeProps> = ({ userid, loading, handleInputChange, handleSearch }) => {//変更
Porpsを渡されますので、Homeコンポーネントとしての型定義を追加しています。
Homeの型は、React.FC<HomeProps>となります。HomePropsは先のtypeの箇所で設定した型定義です。
( )の中は、{ userid, loading, handleInputChange, handleSearch } とApp.tsxより渡されるPropsを記載します。
続いて、JSXの箇所です。
// /src/components/Home.tsx
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}//追加、useridを定義
onChange={handleInputChange}//追加、フィールドに入力されたら、handleInputChangeを実行
/>
<Box mt={4} >
<Button
colorScheme='blue'
onClick={handleSearch}//変更、クリックされたら、handleSearchを実行
されたら、
w='100%'
loadingText='Loading'//追加、ローティング状態時のテキスト指定
isLoading={loading}//追加、ローティング状態をloadingの値で判定
spinnerPlacement='start'//追加、ローティング状態時の回転アニメの場所指定
>名刺を表示</Button>
</Box>
<Box mt={4} >
<Button
colorScheme='blue'
variant='outline'
w='100%'>新規登録</Button>
</Box>
</CardBody>
</Card>
</Flex>
</>
)
InputとButtonの箇所にオプションを追加しています。
Input箇所は、valueとしてuseridを設定し、フィールドに値が入力されたら、入力値をuseridとして、App.tsxで定義したhandleInputChangeを呼び出しています。これによりsetUseridにて値がuseridにセットされます。
「名刺を表示」Buttonは、クリックすると、App.tsxで定義したhandleSearchを呼び出し、DBデータを取得します(現時点はDBデータ取得処理はまだ記載してないので、クリックしてもデータ取得はされません)。その他、データ取得中はローディング状態にして、ローディグ中である事がわかるようなスピンアニメーションとLoadingの表記をするよう設定しています。
4.3 Cards.tsx
それでは、Cards.tsxの中身を記載していきます。
以下内容となります(コード全文です。)
// /src/components/Cards.tsx
import { Link, useNavigate } from "react-router-dom";//React Router、Link, useNavigateのインポート
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Icon, Text } from "@chakra-ui/react";//Chakra UIパーツのインポート
import { CardData } from "../cardData";//CardData型定義のインポート
import { FaGithub } from "react-icons/fa";//GitHubアイコンのインポート
import { SiQiita } from "react-icons/si";//Qiitaアイコンのインポート
import { FaXTwitter } from "react-icons/fa6";//Xアイコンのインポート
type CardsProps = {
cardData: CardData[];
setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
}
const Cards: React.FC<CardsProps> = ({ cardData, setCardData }) => {
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'>
<Button
colorScheme='blue'
mr='3'
>編集
</Button>
<Button
colorScheme='orange'
variant='outline'
mr='3'>
削除</Button>
<Button
colorScheme='gray'
variant='outline'
onClick={() => {
setCardData([]);
navigate('/');
}
}>戻る</Button>
</Box>
</CardBody>
</Card>
</Flex>
)
}
export default Cards;
解説していきます。
まずは冒頭のインポート箇所と、type設定、コンポーネント定義の箇所です。
// /src/components/Cards.tsx
import { Link, useNavigate } from "react-router-dom";//React Router、Link, useNavigateのインポート
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Icon, Text } from "@chakra-ui/react";//Chakra UIパーツのインポート
import { CardData } from "../cardData";//CardData型定義のインポート
import { FaGithub } from "react-icons/fa";//GitHubアイコンのインポート
import { SiQiita } from "react-icons/si";//Qiitaアイコンのインポート
import { FaXTwitter } from "react-icons/fa6";//Xアイコンのインポート
type CardsProps = {
cardData: CardData[];//cardData.tsで定義された型を持つ配列
setCardData: React.Dispatch<React.SetStateAction<CardData[]>>
}
const Cards: React.FC<CardsProps> = ({ cardData, setCardData }) => {
const navigate = useNavigate();//useNavigateの利用
インポートについては、記載の通りです。
React Routerについては、useNavigateに加え、Link機能をインポートします。これはaタグと同等のものとなりますが、クリックした際にページ全体をリロードするaタグとは異なりページ内の更新が必要な箇所のみ更新を行うのがLinkコンポーネントです。
GitHub、Qiita、Xについては、React Iconsのパーツを利用しますので、そのインポートをしています。
type設定については、App.tsxからcardData, setCardDataがPropsとして渡されますので、その定義を行っています。cardDataは、CardData(/src/cardData.ts)で定義された型情報を持った、配列です。
setCardDataは4.2で記載したVSCode上でマウスオーバーした際にポップアップ表示される型情報を記載しています。
Cardsコンポーネントの型定義については、先のHome.tsxと同様ですが、React.FC<CardsProps>となります。( )の中は、{ cardData, setCardData } とApp.tsxより渡されるPropsを記載します。
const navigate = useNavigate() は、React RouterのuseNavigateを利用する為のconst定義です。
続いて、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>//user_idを表示
<Heading size='sm' textTransform='uppercase'>
名前
</Heading>
<Text pb='2' fontSize='sm'>{card.name}</Text>//nameを表示
<Heading size='sm' textTransform='uppercase'>
自己紹介
</Heading>
<Text pb='2'
dangerouslySetInnerHTML={{ __html: card.description }}//HTMLデータをHTMとして表示
/>
<Heading size='sm' textTransform='uppercase'>
好きな技術
</Heading>
<Text pb='2' fontSize='sm'>{card.skills}</Text>//skillsを表示
<Flex wrap='nowrap' justifyContent='center' width='100%' >
<Link to={`https://github.com/${card.github_id}`} target='_blank'>
//github_idをリンク先として、gitHubへのリンクを設定
<Icon
as={FaGithub}
fontSize="24px"
margin={1}
_hover={{ transform: 'scale(1.2)', transition: 'transform 0.3s' }}
//hover時に拡大表示(1.2倍)効果を適用
/>
</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>
<Button
colorScheme='orange'
variant='outline'
mr='3'>
削除</Button>
<Button
colorScheme='gray'
variant='outline'
onClick={() => {
setCardData([]);//戻るボタンクリックでcardDataを初期化
navigate('/');//及び、"/"に移動
}
}>戻る</Button>
</Box>
</CardBody>
</Card>
</Flex>
)
Chakra UIの<Card>コンポーネントで構成しています。<CardBody>の内容は、
{cardData.map((card, index) => ( で、mapによりcardData配列の中身をレンダリングしてます。
各変数は、4.1.1で触れた、グレイヴ・アクセント(` `)を利用して埋め込んでいます。
card.description の箇所は、dangerouslySetInnerHTML={{ __html: card.description }}とあり、このフィールドはHTMLタグを許容する形となってるのですが、そのHTMLデータをHTMLとして表示させるためのメソッドです。
が、dangerouslySetInnerHTMLと言う名前通り、セキュリティリスクがあるものとなってます。HTMLが埋め込めるため、XSS攻撃などのリスクが発生します。
ここでは、このまま利用していきますが、これを安全化するサニタイズの手法等もあるようです。
<Button>として、削除ボタンと戻るボタンを設置しています。削除ボタンは今時点は機能しません。後で、削除機能を作成する際に実装します。
戻るボタンは、ocClickでcardData初期化とuseNavigateによる”/”への遷移を設定しています。
この時点で以下のような画面の動きが実現出来ると思います。なお、HomeのIDの箇所は、今のところ、selectDbでtrueしか返さない設定となってますので、何かしら入力すればCards画面に遷移します。がデータは何も取得しないので、中身は何もない状態となります。ID自体を入力してない場合は、Chakra UIのトーストでエラー表示されます。
4.4 データ取得機能の実装
それでは、実際にSupabaseからデータを取得する機能を作成します。現状、空の状態の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";//supabaseクライアントインポート追加
まず、2.3で作成したsupabaseClientのインポートを追加します。
続いて、selectDbにSupabaseのテーブルデータ取得のコードを記載していきますが、ここは少々複雑です。Card.tsxで記載したデータ表示に必要な情報とSupabaseのテーブルとの関係は下記の通りとなります。
必要な情報 | 情報があるテーブル |
---|---|
user_id | users, user_skill |
name | users |
description | users |
skills | skills(nameカラム) |
github_id | users |
qiita_id | users |
x_id | users |
この内、skillsについてですが、今回テーブルに予め登録した、sample_idの場合、sample_idのskill_idはuser_skillテーブルにあり、その値は1です。
skillsはskillsテーブル上でidに紐付く形で、nameで定義されています。
user_skillsのskill_idが1の場合は、skillsは、skillsテーブル、id:1のReactとなります。
(下図参照ください)
この構造を踏まえて、Supabaseのテーブルから情報を取得するプロセスは以下のようになります。
- Home画面で入力されたuseridをキーにマッチするデータをusersテーブルから取得
- 同様にuseridをキーにマッチするskill_idをuser_skillテーブルから取得
- user_skillテーブルから取得したskill_idにマッチするnameデータをskillsテーブルから取得、そのデータをskillsとして設定
それでは、selectDbのコードを見ていきます。
// /src/App.tsx
// Supabaseからデータを取得する関数
const selectDb = async (): Promise<boolean> => {
//タイムラグがが想定されるので、非同期通信async/awaitで実装。結果成否を明確にするためboolean型で定義
setLoading(true);
// Step 1: usersテーブルからuseridにマッチする全データを取得
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,//skill_idを編集画面で利用する為、cardData用に追加
}));
console.log('step4', combinedData)
setCardData(combinedData);
toast({
title: 'データを取得しました',
position: 'top',
status: 'success',
duration: 2000,
isClosable: true,
})
setLoading(false);
return true; // データ取得に成功した場合はtrueを返す
};
処理のステップを4つ設けてます。
- usersテーブルからuseridにマッチする全データを取得
- user_skillテーブルからuseridにマッチするskill_idを取得
- skillsテーブルからskill_idにマッチするidのnameを取得
- 各ステップで取得したデータを集約
なお、SupabaseはPostgreSQLをベースにしたRDBですので、複数テーブルの結合は外部キーの設定で可能で、上記のような複雑なプロセスを使わなくても一括して複数テーブルに跨るデータの取得は可能です。が、そちらも試行してたのですが、今ひとつ意図通りにならず、ステップbyステップの処理としました。
まず冒頭の箇所及びStep1についてです。
// /src/App.tsx
// Supabaseからデータを取得する関数
const selectDb = async (): Promise<boolean> => {
//タイムラグがが想定されるので、非同期通信async/awaitで実装。結果成否を明確にするためboolean型で定義
setLoading(true);//ローディング状態をローディング中にセット
// Step 1: usersテーブルからuseridにマッチする全データを取得
const { data: userData, error: userError } = await supabase//supabaseClientによるsupabaseとの連携処理実行
.from('users')
.select('*')
.eq('user_id', userid);//useridをキーにusersテーブルより全データ取得
if (userError || !userData || userData.length === 0) {
//エラー発生、または、取得したuserDataが空であればChakura UIのトーストでエラー表示
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)//成功したら、コンソールにstep1として取得したデータを表示
内容は上記コードのコメントに記載した通りです。
関数の型としては、試行錯誤の過程で、どうもうまく行かないケースがあったので、明示的に成否を返すboolean型として、失敗時はfalseをreturnさせてます(最終的に全ステップ通ればtrueを返します)。
最初にローディング状態をローディング中にセットします。
次にsupabase関数(2.3で作成した/src/supabaseClient.ts によるsupabaseクライアント機能)にてDBに接続し、データを取得しています。
エラー発生時、または取得したデータが空等の場合は、Chakra UIのトースト機能でエラーメッセージを表示させています。またその時点で、ローディングを解除し、処理失敗としてfalseをリターンしてます。処理成功時は、console.log でStep1の取得データをコンソールに表示させています。
次にStep2です。
// /src/App.tsx
// Step 2: user_skillテーブルから該当するskill_idを取得
const { data: userSkillData, error: userSkillError } = await supabase
.from('user_skill')
.select('skill_id')
.eq('user_id', userid);//useridをキーにuser_skillテーブルよりskill_id取得
if (userSkillError || !userSkillData || userSkillData.length === 0) {
//エラー発生、または、取得したuserDataが空であればChakura UIのトーストでエラー表示
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)//成功したら、コンソールにstep2として取得したデータを表示
const skill_ids = userSkillData.map((skill) => skill.skill_id);//取得したデータをmapで回し、skill_idsに格納
Step1と同様ですが、supabase関数で、useridをキーにuser_skillテーブルよりskill_id取得してます。
エラーの場合は、同様にトーストでエラー表示、ローディング状態解除と、falseをリターン。
処理成功時は、コンソールにstep2として取得したデータを表示してます。
最後に取得したデータをskill_idsに格納してます。この際、Supabaseから返されるデータは配列となりますので、mapで回して格納しています(実際のデータは1組しか無いので、配列番号[0]を格納するやり方もあると思います)。
Step3です。
// /src/App.tsx
// Step 3: skill_idsを元にskillsテーブルからnameを取得
const { data: skillsData, error: skillsError } = await supabase
.from('skills')
.select('name')
.eq('id', skill_ids);
if (skillsError || !skillsData || skillsData.length === 0) {
//エラー発生、または、取得したuserDataが空であればChakura UIのトーストでエラー表示
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)//成功したら、コンソールにstep3として取得したデータを表示
const skillsString = skillsData.map(skill => skill.name);//取得したデータをmapで回し、skillsStringに格納
これまでと同じような操作です。supabase関数で、Step2でセットした、skill_idsをキーにskillsテーブルよりnameを取得してます。
エラーの場合は、同様にトーストでエラー表示、ローディング状態解除と、falseをリターン。
処理成功時は、コンソールにstep3として取得したデータを表示してます。
最後に取得したデータをskillsStringに格納してます。この際、Supabaseから返されるデータは配列となりますので、mapで回して格納しています(Step2と同様、実際のデータは1組しか無いので、配列番号[0]を格納するやり方もあると思います)。
最後のStep4です。
// /src/App.tsx
// Step4:結果をcombinedDataに集約
const combinedData = userData.map((user) => ({//取得したデータをmapで回し、Step1のデータにStep2,3のデータを柄
...user, skill_id: skill_ids, skills: skillsString,//skill_idは、編集画面で利用する為、追加
}));
console.log('step4', combinedData)//コンソールにstep4として集約したデータを表示
setCardData(combinedData);//集約したデータをcardDataにセット
toast({
title: 'データを取得しました',
position: 'top',
status: 'success',
duration: 2000,
isClosable: true,
})
setLoading(false);//ローディング状態を解除
return true; // データ取得に成功した場合はtrueを返す
};
Step1,2,3と処理して取得したデータをこのStep4で集約しています。combinedDataと定義した定数に、mapを回して、Step1データにStep2、3のデータを追加しています。これも配列の為の処理で、実際は1個の配列データなので、[0]のみの操作でもいいと思います。
なお、skill_idはCardsでの表示には利用しないものですが、データ新規登録や編集時の際のスキル選択肢として利用しますので、格納する形にしています。
コンソールにstep4として集約したデータを表示し、combinedDataとして集約したデータをuseStateのsetCardDataでcardDataにセットしてます。
また、データ取得成功として、Chakra UIのトーストで「データを取得しました」のメッセージ表示を設定しています。最後に全Step処理完了時に、ロディング状態を解除し、trueをリターンしています。
selectDbを利用した処理は既に組み込み済ですので、他のコンポーネントの変更箇所はありません。
この時点で以下の画面の動きとなります。
コンソールにも以下のように各Step毎の取得データが表示されます。
ここまでで、Supabase上のテーブルからデータを取得し表示する機能が実現できました。前編はここまでとします。
後編では、新規データ登録機能、データ編集・削除機能、GitHub、Firebase連携について説明したいと思います。
No responses yet