チュートリアル 学習記録アプリ、3回目です。前回まででSupabaseを活用したDBデータの読み込み・編集・削除を実装しました。
今回は、Supabaseの認証機能を利用してユーザー認証の仕組みを設けてみたいと思います。内容が長くなってしまったので、前編・後編と2回に分けてお届けします。
今回は前編となります。前編ではSupabaseの設定、環境構築、サインアップ機能の開発までを解説します。後編では、ログイン・ログアウト、パスワードリセット機能の実装を解説します。
なお、1回目、2回目の記事は以下となります。
後編公開しました!
目次
はじめに
本記事は、Reactの初学者向けのチュートリアルコンテンツです。初学者向けとは言え、データの格納や認証にBaaS(Backend as a Service)であるSupabaseを利用したものとなっており、バックエンド処理も入ってきてますので、難易度はやや高いかも知れません。
Reactコードの記述、SupabaseのDBデータ処理、認証処理の実装、及び実際にアプリケーションをデプロイしてユーザーが使えるリリースまでを行えます。
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の環境設定とテーブル処理(表示、登録、更新、削除)
- Supabaseの認証設定と、認証制御、セッション管理
- GitHubリポジトリの扱い
- Firebaseホスティングの利用、GitHubと連携した自動デプロイ 等
今回は、前編と言うことで、Supabaseの設定、環境構築、サインアップ機能の開発をしていきます。
また、1回目、2回目で割と細かく説明してた箇所は、適宜、省略することもありますので、状況に応じて、以前の内容を参照いただければと思います。
なお、今回、Supabaseのユーザー認証の実装にあたって、参考にしたサイトは以下となります。
Next.jsを前提にした記事ですが、内容はReactコードですので、十分に参考になります。
その他、補足として下記のサイトとSupabaseのドキュメントを参考にしています。
1. Supabaseの設定
Supabaseのアカウントやプロジェクトは既にある前提で進めます。お持ちでない方はこちらを参照して作成してください。
1.1 ユーザ認証設定
まずは、Supabaseのサイトでユーザー認証の設定を行います。今回はベーシックなEmailとpasswordによる認証を実装したいと思います。
Supabaseのサイトで、左側アイコンをhover時に表示される、Authenticationを選択し、Providersを選択します。冒頭にEmailがありますので、Enabledにします(おそらくデフォルトでEnabledになってるかと思います)。それぞれのオプションは下図のとおりに。なお、Email OTP ExpirationはSupabase推奨の3600秒で設定してください(オーバーしてると警告が出ます)。
1.2 カスタムSMTP設定
ユーザー新規登録時やパスワードリセット時のメール送信について、これまでは、Supabaseが準備しているSMTP機能でメール送信が可能でしたが、2024/9/26より、これに制限が加わり、プロジェクトに存在しているユーザーのみにメール送信が可能と言う仕様に変更されました・・・。これは不正利用を抑止・防止するための措置だそうです。
実質、新たにユーザーを作成する場合は、これまでのSupabaseのSMTPは使えないと言うことになります。
よって、メールによる認証を行うためには、個別にSMTPの設定が必要になります。これは、悩ましいですが、SMTPのサービスを提供しているResendや、AWSのSESの活用が考えられますが、結果的に私は自分のメールアドレスのSMTPサーバを使う形にしました。
カスタムSMTPの設定は、左側メニューの下側、歯車アイコンのProject Settings > Authenticationの箇所にあります。そこの2つ目に「SMTP Settings」がありますので、「Enable Custom SMTP」を有効にします。その上で、SMTPサーバの設定に必要な内容を入力します。これはSMTPプロバイダーによって異なってくると思います。入力後は保存します。
1.3 新規登録時のメールテンプレート
続いて、ユーザー新規登録時に、登録確認・完了処理のため、送信されるメールテンプレートの設定です。Supabaseのサイトで、左側アイコンをhover時に表示される、Authenticationを選択し、Email Templatesを選択します。Confirm signup の箇所が新規登録時に送信されるメールテンプレートです。
この内容はデフォルトでもいいかと思いますが、カスタマイズする場合は、下の Source の箇所を編集します。最後に一番下にある、緑色の「Save」ボタンをクリックします。
これでユーザー新規登録時に、以下のようなメールが届き、登録完了クリックにより、Supabase上でユーザー登録が完了します。
Supabase上のAuthentication > Usersを選択すると、登録されたユーザ一覧が表示されます。
まだリンクでの認証が済んでいない場合は、Last Sign Inの列でWaiting gor verificationと黄色で表示されます。
1.4 パスワードリセット時のメールテンプレート
次に、パスワードリセット処理時にユーザーにリセット用URLを案内するメールテンプレートの設定です。まず、申請メール中にあるリンクのリダイレクト先ドメインを設定します。
後ほど、開発を進めるメール送信部分のresetPasswordForEmail関数で設定したリダイレクト先のURLドメインを設定するものです。SupabaseのAuthenticationを選択し、URL ConfigurationのRedirect URLsにリダイレクト先のドメインを登録します。
なお、ここでは、ひとまず、Viteの開発要サーバ、http://localhost:5173/*で設定します。
これは、後にFirebaseでデプロイし、アプリ公開をする場合は、変更が必要となります。
そして、先程のようにメールテンプレートを設定します。予めテンプレートはセットされてますので、カスタマイズしたい場合に変更します。
Authentication > Email Templates、Reset Passwordを選択します。変更後、下にある「Save」ボタンをクリックして保存します。
下記のような案内メールが送信されます。クリックすると、後ほど解説する、パスワードリセット画面にアクセスできます。
1.5 テーブルの準備
ここまでで、Supabaseのユーザー認証周りの設定は実施しましたので、次に利用するテーブルの準備です。作成したプロジェクトを選択の上、左側にhover時表示されるメニューから「Table Editor」を選択します。
ここではテーブルは新規作成してますが、前回作成したテーブルをDuplicateして利用するのもありだと思います。ただし、ポリシーは新規に作成し直しが必要です。
「Create a new table]を選択します。
Nameは「learning_record_auth」としました(※お好きなもので結構です)。
Descriptionは任意で
Enable Row Level Security (RLS)Recommended は、デフォルトのONのままで
Enable Realtime は不要です。
Columnsは以下内容で設定してください。前回から、emailのフィールドを新たに追加しています。これは認証ユーザーのemailを登録し、そのemailがマッチするデータのみをユーザに表示・編集させるようにしたいので、このようにしてます。
Name | Type | Dafault Value | Primary | Extra Options |
---|---|---|---|---|
id | uuid | gen_random_uuid() | ✓ | |
title | varchar | 選択なし | ||
time | init4 | 選択なし | ||
text | 選択なし |
Foreign keysは不要です。
次に作成したtableにデータを作成しておきます。
左メニューのTable Editorから「learning_record」(作成したテーブル)を選択し、insert → insert lowをクリックします。
右側にデータの内容を入力するエリアが表示されますので、データを入力します。ひとまず、初期データとして下記のReactとTypeScriptの2つセットします(idは自動でセットされますので、入力不要です)。またセットするメールアドレスは実際にメールを受け取れるアドレスを設定してください。後ほど、ユーザ登録の際に送信されたメールで認証を行う為です。
Supabase上でメール認証を実施しなくても、ユーザーを作成することは可能です(本記事ではそのようにしてtest@test.comを設定してます)。ですが、送信されたメールでの認証処理等、一連の処理を実行するには、実際にメール確認出来るアドレスを利用されるのがお薦めです。
※追記:
1.2で記述の通り2024/9/26以降の仕様変更により、ダッシュボード上でのユーザー作成は出来なくなりました。よってカスタムSMTP等でのメール認証を実施するか、emailではない認証方式でユーザ作成が必要です。
id | title | time | |
---|---|---|---|
自動セット | React | 10 | ユーザ登録に使用するアドレス |
自動セット | TypeScript | 5 | ユーザ登録に使用するアドレス |
「learning_record_auth」の内容が以下のように表示されるかと思います。
1.6 ポリシーの作成
続いて作成したテーブルに対して、ポリシー作成を行います。これはテーブルのデータ操作(SELECT,INSERT,UPDATE,DELETE)に対する権限を設定するものです。
作成テーブル「learning_record」の右側にある、・・・の箇所をクリックすると表示されるメニューから、View Policies を選択します。
表示される画面の右側の、Create Policyをクリックします。
以下画面のように設定していきます。
- Policy Name:任意のポリシー名を入力します。ここではlearning-records policyとしてます。
- Polciy Command:ALLを選択、全ての操作に共通のポリシーとしてます。
- Target Roles:anonと、authenticatedを選択
- Use options above to edit:7行目に、tureと記載
- Use check expression:チェックしない
前回記事のテーブル作成時は、Target Roles:anonとしてましたが、今回はユーザー認証が通ったユーザーにアクセス許可しますので、authenticatedを追加します。なお、anonを残していますが、これは開発途中段階での動作確認の為の暫定です。最終的には認証ユーザのみとしますので、authenticatedのみとするよう、後ほど修正します。
この内容で、画面下にある、Save policyをクリックしてポリシーを保存します。
これでSupabase側の設定は完了です。
2. 環境構築
それでは開発環境の構築をしていきます。今回は、ユーザー認証実装にあたり、サインアップ、ログイン、ログアウト、パスワードリセットと言った機能を開発します。また従来(前回まで)のテーブル表示・編集・追加・削除機能もあります。色々と画面遷移が発生しますので、React Routerを利用します。React Routerは、複数のページを持つReactアプリケーションを構築する際に利用されるライブラリです。React Routerを使うことで、ユーザーの操作に応じて表示内容を変更したり、URLにパラメータやクエリを含めて表示内容を変更したりすることができます。
2.1 アプリ構造
今回作成するアプリの構造は下図のイメージです。
- App.tsx
トップコンポーネントです。 - pages/Login.tsx
ログイン画面です。初期画面となります。ルーティングは”/”で設定します。 - pages/Home.tsx
ログイン後、ログインユーザ用の学習記録をsupabaseより読み込み表示します。ルーティングは”/home”でセットします。 - components/Edit.tsx
前回記事で作成した学習記録を編集・更新するコンポーネントです。モーダルを採用しています。 - components/Delete.tsx
前回記事で作成した学習記録を削除するコンポーネントです。モーダルを採用しています。 - components/NewEntry.tsx
前回記事で作成した学習記録を新規登録コンポーネントです。モーダルを採用しています。 - pages/Logout.tsx
ログアウト用のモーダル。ログアウトすると、初期画面(ログインページ)に遷移します。 - pages/Register.tsx
ユーザーサインアップの画面です。初期画面で新規登録をクリックすると遷移します。ルーティングは”/register”でセットします。 - pages/ResetSend.tsx
パスワード忘れ等、パスワードをリセットする場合の画面です。入力されたメールアドレス宛にリセット画面URLの案内メールを送信します。ルーティングは”/sendReset”でセットします。 - pages/PasswordSend.tsx
パスワードリセット申請を受けたあと、メールで案内されるパスワードリセット用の画面です。ルーティングは”/passwordReset”でセットします。
2.2 初期環境構築
前回までの記事で、Reactプロジェクトの作成、及び必要なライブラリインストールを実施している方は、そのままその環境を利用いただいても結構です。新規のプロジェクトを作成したい場合は、以下の手順を実施してください。なお、JavaScript実行環境のnode.jsとパッケージマネージャのnpmはインストールされている前提としてます。環境がない場合は、以下を参照ください。
まずは、Viteを利用してReactプロジェクトを作成します。ターミナル上で、プロジェクトを作成したいディレクトリに移動して、npm create vite@latest でプロジェクト作成を実行します。
プロジェクト名は「learning-records-auth」としましたが、お好きな名前で結構です。
他の選択肢は下記の通りです。
npm create vite@latest
✔ Project name: … learning-records-auth
✔ Select a framework: › React
✔ Select a variant: › TypeScript
これでReactの初期環境構築は完了です。
続けて出てくる内容に従って、環境起動を行います。
cd learning-records-auth
npm i
npm run dev
以下のように表示されると思います。http://localhost:5173/にアクセスすれば開発環境の画面が表示されます。
VITE v5.4.2 ready in 237 ms<
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
続いて、必要なライブラリをインストールします。Chakura UI、React Icons、そしてReact Routerをインストールします。サーバを起動したターミナルで「ctrl-c」を入力し一度サーバ停止するか、別のターミナルを開いてプロジェクトディレクトリ(learning-records-auth)に移動してインストールします
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion react-icons react-router-dom
Supabaseのパッケージをインストールします。
npm i @supabase/supabase-js
2.3 環境定義ファイル等の作成
それではコードを記載していきます。
まずは、前回記事で作成していた、Supabaseの環境設定ファイルをプロジェクトルート直下に、また、Supabaseのクライアント機能のファイル、学習データの型定義ファイルを/src配下に作成します。
- supabaseの環境設定:/.env
- supabaseのクライアント機能:/src/supabaseClient.ts
- 学習データの型定義:/src/studyData.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/studyData.tsです。下記のコードを記載します。
// /src/studyData.ts
export type StudyData = {
id: string,
title: string,
time: number,
}
2.3 その他、下準備
下準備と記載したのは、前回記事の繋がりからそのように表現してます。前回のコードをベースに解説をしていきます。なお、新規に作成される方も進め方は変わりません。掲載しているコードをそのまま記載頂ければ大丈夫です。
今回はReact Router機能を使っていきます。App.tsxはRouter設定の記述が中心となりますので、前回記載していた、Supabaseから読み込んだテーブルデータ表示箇所は別のコンポーネントに移行します。srcフォルダ配下に、pagesフォルダを作成し、その中に、Home.tsxと言うファイルを作成します。
まず、main.tsxとApp.tsxにReact Routerの設定を記述します。また、App.tsxは、Home.tsxに持って行くテーブル表示の箇所は削除します。
mai.tsxとApp.tsxのコード内容は以下です。
// /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 { BrowserRouter } from 'react-router-dom'//React Routerのインポート
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ChakraProvider>//Chakra UI
<BrowserRouter>//BrowserRouterで囲む
<App />
</BrowserRouter>//BrowserRouterで囲む
</ChakraProvider>//Chakra UI
</StrictMode>,
)
続いて、App.tsxです(下記はコード全体です)。
// /src/App.tsx
import { useState } from "react";
import { Link, Route, Routes } from "react-router-dom";//React Routerのインポート
import { Box, Card, Flex } from "@chakra-ui/react"
import { StudyData } from "./studyData";
import { supabase } from "./supabaseClient";
import Home from "./pages/Home";//Home.tsxインポート
function App() {
const [learnings, setLearnings] = useState<StudyData[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [email, setEmail] = useState('test@test.com');
//emailステートを追加、test@test.comと記載してますが、Supabaseテーブル作成時に設定したメールアドレスを定義してください
// Supabaseからデータを取得する関数
const fetchLearnings = async () => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.select('*')
.eq('email', [email])//email追加、配列として渡す
if (error) {
console.error('Error fetching data:', error);
console.log('データの読込に失敗しました', error);
setError(`データの読込に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('データ読み込み完了', data);
setLearnings(data);
setLoading(false);
}
};
/*App.tsxからは除外、Home.tsxで実装
// useEffectを使ってコンポーネントのマウント時にデータを取得
useEffect(() => {
fetchLearnings();
}, []);
*/
//DB新規登録
const insertDb = async (learning: StudyData) => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.insert([
{ title: learning.title, time: learning.time, email: email },//email追加
])
.select();
if (error) {
console.error('Error insert data:', error);
setError(`データの更新に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('insert', data);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
//DB更新
const updateDb = async (learning: StudyData) => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.update({ title: learning.title, time: learning.time })
.eq('id', learning.id)
.select()
if (error) {
console.error('Error insert data:', error);
setError(`データの更新に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('update', data);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
//DBデータ削除
const deleteDb = async (learning: StudyData) => {
setLoading(true);
const { error } = await supabase
.from('learning_record_auth')
.delete()
.eq('id', learning.id);
if (error) {
console.error('Error delete data:', error);
setLoading(false);
} else {
console.log('delete', learning.id);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
const calculateTotalTime = () => {
return learnings.reduce((total, learning) => total + learning.time, 0);
};
return (
<Routes>
<Route path='/home' element={
<Home
learnings={learnings} loading={loading} setLoading={setLoading} error={error}
setError={setError} deleteDb={deleteDb}
insertDb={insertDb} updateDb={updateDb}
calculateTotalTime={calculateTotalTime}
fetchLearnings={fetchLearnings} email={email} />
}
/>
<Route
path="*"
element={
<Flex justifyContent='center' alignItems='center' p='5'>
<Card p='10'>
みつかりません<br />
<Box
as='span'
textDecoration='none'
_hover={{ textDecoration: 'underline' }}
>
<Link to='/'>トップページへ</Link>
</Box>
</Card>
</Flex>
}
/>
</Routes>
)
}
export default App
App.tsxについて解説します。
まずは、React Routerのインポートと、新しいstate、email, setEmailの追加です。
// /src/App.tsx
import { useState } from "react";
import { Link, Route, Routes } from "react-router-dom";//React Routerのインポート
import { Box, Card, Flex } from "@chakra-ui/react"
import { StudyData } from "./studyData";
import { supabase } from "./supabaseClient";
import Home from "./pages/Home";//Home.tsxインポート
function App() {
const [learnings, setLearnings] = useState<StudyData[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [email, setEmail] = useState('test@test.com');
//emailステートを追加、test@test.comと記載してますが、Supabaseテーブル作成時に設定したメールアドレスを定義してください
インポートの箇所に、React Routerパッケージのインポートを記述します。また、先に作成した、Home.tsxのインポートも追加しておきます。
補足ながら、このimport文はVSCodeをお使いであれば、それに関連するコードを入力することで、自動で補完してくれます。以前の記事に掲載しています。
そして、新たなstateとしてemail, setEmailを追加しています。ログインユーザー情報としてemailを利用してテーブルのデータ操作を行うためです。この段階では、emailは、test@test.comと固定値をセットしています。上記コードではtest@test.comと記載してますが、このメールアドレスは先の1.4 テーブルの準備で設定したメールアドレスを記載してください。動作確認の際、ここで設定されたメールアドレスにマッチしたテーブルデータを抽出する為です。
続いて、DB処理関連の関数についてです。
// /src/App.tsx
// Supabaseからデータを取得する関数
const fetchLearnings = async () => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.select('*')
.eq('email', [email])//email追加、配列として渡す
if (error) {
console.error('Error fetching data:', error);
console.log('データの読込に失敗しました', error);
setError(`データの読込に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('データ読み込み完了', data);
setLearnings(data);
setLoading(false);
}
};
//DB新規登録
const insertDb = async (learning: StudyData) => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.insert([
{ title: learning.title, time: learning.time, email: email },//email追加
])
.select();
if (error) {
console.error('Error insert data:', error);
setError(`データの更新に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('insert', data);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
まず、Supabaseのテーブル「learning_record_auth」のデータを読み込む、fetchLearningsです。
新たに.eq(‘email’, [email]) を追記し、テーブルのemailの欄が、定数emailにマッチするデータを持ってきています。なお、emailの型はstringですので、テーブルからselectする際は配列の形式([ ]で囲む)で渡す必要があります。
続いて、新規データ登録を行う、insertDbですが、新たにinsert(データ追加)の要素として、email: emailを追加しています。
他の関数(updateDb、deleteDb)は、emailの追加等は発生しません。が、前回のコードを流用している方は、操作対象のデーブルが変更となってますので、そこは修正必要ですので、ご注意ください(learning_record → learning_record_auth、テーブルを変更していない方は不要です)。
console.logをあちこちに記載してますが、処理自体には必要ありません。コードがどこまで処理されてるかのトレース目的と、console.logで定数指定しないと、「宣言されてるけど、どこにも使われてない警告」が出たりするので、残してます。
続いて、return以降のJSX分の箇所です。
※JSX(JavaScript XML)は、コンポーネント指向のJavaScriptライブラリやフレームワーク(特にReact)で一般的に採用されている、JavaScriptの拡張構文です。JSXを用いると、JavaScriptのコード内にHTMLタグのような構文が埋め込み可能となり、より直感的かつ読みやすい形でUIのコードを表現することができます。それによって、開発者のコーディング体験や開発、デバッグの効率が上がります。
https://typescriptbook.jp/reference/jsx
// /src/App.tsx
return (
<Routes>
<Route path='/home' element={
<Home
learnings={learnings} loading={loading} setLoading={setLoading} error={error}
setError={setError} deleteDb={deleteDb}
insertDb={insertDb} updateDb={updateDb}
calculateTotalTime={calculateTotalTime}
fetchLearnings={fetchLearnings} email={email} />
}
/>
<Route
path="*"
element={
<Flex justifyContent='center' alignItems='center' p='5'>
<Card p='10'>
みつかりません<br />
<Box
as='span'
textDecoration='none'
_hover={{ textDecoration: 'underline' }}
>
<Link to='/'>トップページへ</Link>
</Box>
</Card>
</Flex>
}
/>
</Routes>
)
JSXの箇所は、React Routerのルート設定を記載しています。
<Routes>で囲まれた箇所に
<Route path=’設定したいパス’ element={読み込むコンポーネント+渡すPorps、もしくはJSXの記載} />
のような書き方をします。
上記のコードでは、/homeのパスとして、Home.tsxをセットしてます。Home.tsxは先述の通り、従来App.tsxに記載していた、Supabaseのテーブルデータ表示及び、その中にセットしていた、編集機能のEdit.tsx、削除機能のDelete.tsx、新規データ登録のNewEntry.tsxの埋め込みを移管しますので、App.tsxからHome.tsxに渡すPropsも他のコンポーネント(Edit.tsx,Delete.tsx,NewEntry.tsx)に渡すPropsも合わせてHome.tsxに渡します。他のコンポーネントはHome.tsx経由でPropsを受け取る形です。と言うことで<Home …の箇所は長いPropsの羅列になってます。
次にルート設定として<Route path=”*” を設定しています。これは、ルート設定のいずれも当てはまらない全てのURLの表示を指定しています。いわゆる、404ページです。記載内容としては、みつかりません、としてトップページへのリンクを表示しています。
なお、この段階では、ルート設定は/homeしか無いので、たとえ、ルート直下の/にアクセスしても、みつかりません。が表示されることになります。
続いて、Home.tsxに以下コードを記載します(以下はコード全体です)。
// /src/pages/Home.tsx
import { useEffect } from "react"
import { Box, Card, CardBody, CardHeader, Flex, Heading, Spinner, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"
import Edit from "../components/Edit"
import Delete from "../components/Delete"
import NewEntry from "../components/NewEntry"
import { StudyData } from "../studyData"
type Props = {
learnings: StudyData[],
insertDb: (learning: StudyData) => Promise<void>
updateDb: (learning: StudyData) => Promise<void>,
deleteDb: (learning: StudyData) => Promise<void>
loading: boolean,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
error: string,
setError: React.Dispatch<React.SetStateAction<string>>,
calculateTotalTime: () => number,
fetchLearnings: () => Promise<void>,
email: string,
}
const Home: React.FC<Props> = ({ learnings, insertDb, updateDb, deleteDb, loading, setLoading, error, setError, calculateTotalTime, fetchLearnings, email }) => {
// useEffectを使ってコンポーネントのマウント時にデータを取得、Home.tsxマウント時に実行
useEffect(() => {
fetchLearnings();
}, []);
return (
<>
<Flex alignItems='center' justify='center' p={5}>
<Card size={{ base: 'sm', md: 'lg' }}>
<Box textAlign='center' mb={2} mt={6}>
ようこそ!{email} さん
</Box>
<CardHeader>
<Heading size='md' textAlign='center'>Learning Records</Heading>
</CardHeader>
<CardBody>
<Box textAlign='center'>
学習記録
{loading && <Box p={10}><Spinner /></Box>} {/*ローティング中であれば<Spinner />を表示*/}
{error && <Box p={10} color='red'>{error}</Box>}{/*エラーであればエラー内容を表示*/}
<TableContainer>
<Table variant='simple' size={{ base: 'sm', md: 'lg' }}>
<Thead>
<Tr>
<Th>学習内容</Th>
<Th>時間(分)</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{learnings.map((learning, index) => (
<Tr key={index}>
<Td>{learning.title}</Td>
<Td>{learning.time}</Td>
<Td>
<Edit learning={learning} updateDb={updateDb} loading={loading} error={error} setError={setError} />
</Td>
<Td>
<Delete learning={learning} deleteDb={deleteDb} loading={loading} />
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
<Box p={5}>
<div>合計学習時間:{calculateTotalTime()}分</div>
</Box>
<Box p={25}>
<NewEntry learnings={learnings} insertDb={insertDb} updateDb={updateDb} loading={loading} error={error} setError={setError} />
</Box>
</CardBody>
</Card>
</Flex>
</>
)
}
export default Home
それぞれの箇所をみてみます。
これまで、App.tsxに記載していた、Supabaseから読み込んだ学習記録データの表示を移管しています。伴い、データの編集・削除・新規登録(Edit.tsx,Delete.tsx,NewEntry.tsx)コンポーネントを読み込んでいます。これにより、Typesの設定及び、Propsも、Edit.tsx,Delete.tsx,NewEntry.tsxに渡すもの含めた定義となっています。また、新しいstate、emailも定義します。
// /src/pages/Home.tsx
import { useEffect } from "react"
import { Box, Card, CardBody, CardHeader, Flex, Heading, Spinner, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"
import Edit from "../components/Edit"//Edit.tsxのインポート
import Delete from "../components/Delete"//Delete.tsxのインポート
import NewEntry from "../components/NewEntry"//NewEntry.tsxのインポート
import { StudyData } from "../studyData"
type Props = {//各Propsの型定義+新規stateのemailの型定義
learnings: StudyData[],
insertDb: (learning: StudyData) => Promise<void>
updateDb: (learning: StudyData) => Promise<void>,
deleteDb: (learning: StudyData) => Promise<void>
loading: boolean,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
error: string,
setError: React.Dispatch<React.SetStateAction<string>>,
calculateTotalTime: () => number,
fetchLearnings: () => Promise<void>,
email: string,
}
const Home: React.FC<Props> = ({ learnings, insertDb, updateDb, deleteDb, loading, setLoading, error, setError, calculateTotalTime, fetchLearnings, email }) => {//Edit,Delete,NewEntryに渡すProps含め定義+email
元々はApp.tsxに記載していた、マウント時のテーブルデータ取得・表示の処理もHome.tsxで実装しています。
// /src/pages/Home.tsx
// useEffectを使ってコンポーネントのマウント時にデータを取得、Home.tsxマウント時に実行
useEffect(() => {
fetchLearnings();
}, []);
return後のJSX部分です。App.tsx時のものをそのまま移行している感じです。
// /src/pages/Home.tsx
return (
<>
<Flex alignItems='center' justify='center' p={5}>
<Card size={{ base: 'sm', md: 'lg' }}>
<Box textAlign='center' mb={2} mt={6}>
ようこそ!{email} さん
</Box>
<CardHeader>
<Heading size='md' textAlign='center'>Learning Records</Heading>
</CardHeader>
<CardBody>
<Box textAlign='center'>
学習記録
{loading && <Box p={10}><Spinner /></Box>} {/*ローティング中であれば<Spinner />を表示*/}
{error && <Box p={10} color='red'>{error}</Box>}{/*エラーであればエラー内容を表示*/}
<TableContainer>
<Table variant='simple' size={{ base: 'sm', md: 'lg' }}>
<Thead>
<Tr>
<Th>学習内容</Th>
<Th>時間(分)</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{learnings.map((learning, index) => (
<Tr key={index}>
<Td>{learning.title}</Td>
<Td>{learning.time}</Td>
<Td>
<Edit learning={learning} updateDb={updateDb} loading={loading} error={error} setError={setError} />
</Td>
<Td>
<Delete learning={learning} deleteDb={deleteDb} loading={loading} />
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
<Box p={5}>
<div>合計学習時間:{calculateTotalTime()}分</div>
</Box>
<Box p={25}>
<NewEntry learnings={learnings} insertDb={insertDb} updateDb={updateDb} loading={loading} error={error} setError={setError} />
</Box>
</CardBody>
</Card>
</Flex>
</>
この時点で、http://localhost:5173/ にアクセスしてみます。そうすると、「みつかりません」と表示されます。これは前述の通り、まだ、”/”に対するルートの設定をしていないためです。
次にルート設定のある、http://localhost:5173/home にアクセスしてみます。そうすると、以下の画面が表示されます(test@test.comの箇所は、emailのstateで設定したメールアドレスが表示されます)。
App.tsxで定義した、fetchLearningsにより、Supabaseのテーブルからemailにマッチしたデータを抽出し、表示させています。
また、その他の、編集、削除、新規データ登録も、問題なく動作すると思います。Supabaseのテーブル上も操作内容が反映されると思います。
これで、main.tsx、App.tsx、Home.tsxの下準備は完了です。
なお、Edit.tsx, Delete.tsx, NewEntry.tsx については、特に変更はありません。コードは以下に掲載しておきます。内容については、前回記事を参照ください。
// /src/components/Edit.tsx
import React, { useRef, useState } from 'react';
import { Box, Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react';
import { FiEdit } from "react-icons/fi"
import { StudyData } from '../studyData';
type Props = {
learning: StudyData
updateDb: (learning: StudyData) => Promise<void>
loading: boolean
error: string
setError: (error: string) => void
}
const Edit: React.FC<Props> = ({ learning, updateDb, loading, error, setError }) => {
const { isOpen, onOpen, onClose } = useDisclosure()
const initialRef = useRef(null)
const [localLearning, setLocalLearning] = useState(learning)
const handleChangeTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalLearning({
...localLearning,
title: e.target.value
})
}
const handleChangeTime = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalLearning({
...localLearning,
time: Number(e.target.value)
})
}
const handleUpdate = async () => {
await updateDb(localLearning);
if (!loading) {
setTimeout(() => {
onClose();
}, 500);
}
}
return (
<>
<Button variant='ghost' onClick={() => {
setLocalLearning(learning)
onOpen()
}}><FiEdit color='black' /></Button>
<Modal
initialFocusRef={initialRef}
isOpen={isOpen}
onClose={onClose}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>記録編集</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<FormControl>
<FormLabel>学習内容</FormLabel>
<Input
ref={initialRef}
placeholder='学習内容'
name='title'
value={localLearning.title}
onChange={handleChangeTitle}
onFocus={() => setError("")}
/>
</FormControl>
<FormControl mt={4}>
<FormLabel>学習時間</FormLabel>
<Input
type='number'
placeholder='学習時間'
name='time'
value={localLearning.time}
onChange={handleChangeTime}
onFocus={() => setError("")}
/>
</FormControl>
<div>入力されている学習内容:{localLearning.title}</div>
<div>入力されている学習時間:{localLearning.time}</div>
{error &&
<Box color="red">{error}</Box>}
</ModalBody>
<ModalFooter>
<Button
isLoading={loading}
loadingText='Loading'
spinnerPlacement='start'
colorScheme='blue'
variant='outline'
mr={3}
onClick={() => {
if (localLearning.title !== "" && localLearning.time > 0) {
handleUpdate()
}
else setError("学習内容と時間を入力してください")
}}
>
データを更新
</Button>
<Button onClick={() => {
setError("") //エラーを初期化してモーダルクローズ
onClose()
}}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
export default Edit;
// /src/components/Delete.tsx
import React, { useRef } from 'react';
import { Box, Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react';
import { MdDelete } from 'react-icons/md';
import { StudyData } from '../studyData';
type Props = {
learning: StudyData
deleteDb: (learning: StudyData) => Promise<void>
loading: boolean
}
const Delete: React.FC<Props> = ({ learning, deleteDb, loading }) => {
const { isOpen, onOpen, onClose } = useDisclosure()
const initialRef = useRef(null)
const handleDelete = async () => {
await deleteDb(learning);
if (!loading) {
setTimeout(() => {
onClose();
}, 500);
}
}
return (
<>
<Button variant='ghost'
onClick={onOpen}
>
<MdDelete color='black' /></Button>
<Modal
isOpen={isOpen}
onClose={onClose}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>データ削除</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<Box>
以下のデータを削除します。<br />
学習内容:{learning.title}、学習時間:{learning.time}
</Box>
</ModalBody>
<ModalFooter>
<Button
isLoading={loading}
loadingText='Loading'
spinnerPlacement='start'
ref={initialRef}
colorScheme='red'
variant='outline'
mr={3}
onClick={handleDelete}
>
削除
</Button>
<Button onClick={onClose}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
export default Delete;
// /src/components/NewEntry.tsx
import { useRef, useState } from "react";
import { Box, Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Stack, useDisclosure } from "@chakra-ui/react";
import { StudyData } from "../studyData";
type Props = {
learnings: StudyData[]
insertDb: (learning: StudyData) => Promise<void>
updateDb: (learning: StudyData) => Promise<void>
loading: boolean
error: string
setError: (error: string) => void
}
const NewEntry: React.FC<Props> = ({ learnings, insertDb, updateDb, loading, error, setError }) => {
const [learning, setLearning] = useState<StudyData>({ id: "", title: "", time: 0 })
const { isOpen, onOpen, onClose } = useDisclosure()
const initialRef = useRef(null)
const handleEntry = async (data: StudyData) => {
if (learnings.some((l) => l.title === data.title)) {
const existingLearning = learnings.find((l) => l.title === data.title);
if (existingLearning) {
existingLearning.time += data.time;
await updateDb(existingLearning);
}
} else {
await insertDb(data);
}
setLearning({ id: "", title: "", time: 0 });
if (!loading) {
setTimeout(() => {
onClose();
}, 500);
}
}
return (
<>
<Stack spacing={3}>
<Button
colorScheme='blue'
variant='outline'
onClick={onOpen}
>
新規データ登録
</Button>
</Stack>
<Modal
initialFocusRef={initialRef}
isOpen={isOpen}
onClose={onClose}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>新規データ登録</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<FormControl>
<FormLabel>学習内容</FormLabel>
<Input
ref={initialRef}
name='newEntryTitle'
placeholder='学習内容'
value={learning.title}
onChange={(e) => {
setLearning({ ...learning, title: e.target.value })
}}
onFocus={() => setError("")}
/>
</FormControl>
<FormControl mt={4}>
<FormLabel>学習時間</FormLabel>
<Input
type='number'
name='newEntryTime'
placeholder='学習時間'
value={learning.time}
onChange={(e) => {
setLearning({ ...learning, time: Number(e.target.value) })
}}
onFocus={() => setError("")}
/>
</FormControl>
<div>入力されている学習内容:{learning.title}</div>
<div>入力されている学習時間:{learning.time}</div>
{error &&
<Box color="red">{error}</Box>}
</ModalBody>
<ModalFooter>
<Button
isLoading={loading}
loadingText='Loading'
spinnerPlacement='start'
colorScheme='blue'
variant='outline'
mr={3}
onClick={() => {
if (learning.title !== "" && learning.time > 0) {
handleEntry(learning)
}
else setError("学習内容と時間を入力してください")
}}
>
登録
</Button>
<Button onClick={() => {
setError("") //エラーを初期化してモーダルクローズ
onClose()
}}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
export default NewEntry
3. サインアップ機能
続いて、Supabase、ユーザー認証のサインアップ(ユーザー登録)機能を開発していきます。
3.1 サインアップコンポーネントの作成
pagesフォルダ配下に、Register.tsxと言うファイルを作成してください。
Register.tsxに、以下のコードを記載します(下記はコード全文です)。
// /src/pages/Register.tsx
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input, InputGroup, InputLeftElement, Text } from "@chakra-ui/react";
import { useState } from "react";
import { FaUserCheck } from "react-icons/fa";//ユーザーアイコン
import { RiLockPasswordFill } from "react-icons/ri";//パスワードアイコン
import { useNavigate } from "react-router-dom";
import { supabase } from "../supabaseClient";
type Props = {
email: string;
setEmail: React.Dispatch<React.SetStateAction<string>>;
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
const Register: React.FC<Props> = ({ email, setEmail, loading, setLoading }) => {
const [password, setPassword] = useState('')
const [passwordConf, setPasswordConf] = useState('')//パスワード確認用のstate
const [error, setError] = useState('')
const navigate = useNavigate();
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== passwordConf) {
setError('パスワードが一致しません');
return;
} else if (password.length < 6) {
setError('パスワードは6文字以上にしてください');
return;
}
else {
setLoading(true);
try {
const { error: signUpError } = await supabase.auth.signUp({
email: email,
password: password,
})
if (signUpError) {
throw signUpError;
}
alert('登録完了メールを確認してください');
navigate('/'); // signUpが成功した場合にのみページ遷移
}
catch (error) {
setError('サインアップに失敗しました');
}
finally {
setLoading(false);
}
}
};
return (
<>
<Flex alignItems='center' justify='center' p={5}>
<Card px={5}>
<CardHeader>
<Heading size='md' textAlign='center'>新規登録</Heading>
</CardHeader>
<CardBody w={{ base: 'xs', md: 'lg' }}>
<form onSubmit={onSubmit}>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<FaUserCheck color='gray' />
</InputLeftElement>
<Input
autoFocus
type='email'
placeholder='メールアドレスを入力'
name='email'
value={email}
required
mb={2}
onChange={e => setEmail(e.target.value)}
onFocus={() => setError('')}
/>
</InputGroup>
<Text fontSize='12px' color='gray'>パスワードは6文字以上</Text>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<RiLockPasswordFill color='gray' />
</InputLeftElement>
<Input
type='password'
placeholder='パスワードを入力'
name='password'
value={password}
required
mb={2}
onChange={e => setPassword(e.target.value)}
onFocus={() => setError('')}
/>
</InputGroup>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<RiLockPasswordFill color='gray' />
</InputLeftElement>
<Input
type='password'
placeholder='パスワードを入力(確認)'
name='password'
value={passwordConf}
required
mb={2}
onChange={e => setPasswordConf(e.target.value)}
onFocus={() => setError('')}
/>
</InputGroup>
{error && <Box color='red'>{error}</Box>}
<Box mt={4} mb={2} textAlign='center'>
<Button
type="submit"
isLoading={loading}
loadingText='Loading'
spinnerPlacement='start'
colorScheme='cyan'
variant='outline'
mx={2}>登録する</Button>
<Button
colorScheme='gray'
onClick={() => window.history.back()}
mx={2}
>戻る</Button>
</Box>
</form>
</CardBody>
</Card>
</Flex>
</>
)
}
export default Register
Register.tsxの内容について解説していきます。
// /src/pages/Register.tsx
import { Box, Button, Card, CardBody, CardHeader, Flex, Heading, Input, InputGroup, InputLeftElement, Text } from "@chakra-ui/react";
import { useState } from "react";
import { FaUserCheck } from "react-icons/fa";//ユーザーアイコン
import { RiLockPasswordFill } from "react-icons/ri";//パスワードアイコン
import { useNavigate } from "react-router-dom";
import { supabase } from "../supabaseClient";
type Props = {
email: string;
setEmail: React.Dispatch<React.SetStateAction<string>>;
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
冒頭のインポートは、それぞれ必要なものをインポートしていきます(前述の通りVScodeの補完で自動インポートさせるやり方もあります)。Chakra UIの利用コンポーネント、useState、アイコンの他、React Routerの機能であるuseNavigate、個別で作成した、Supabase接続処理用のsupabaseClientをインポートします。
type設定については、email, setEmail, loading, setLoading をApp.tsxよりPropsで受け取りますので、その型定義です。set関数の型は、React.Dispatch<React.SetStateAction<セットする定数の型名>>と、こう言う書き方をするものと思ってください。
続いて、コンポーネント関数の定義とstate関連です。
// /src/pages/Register.tsx
const Register: React.FC<Props> = ({ email, setEmail, loading, setLoading }) => {
const [password, setPassword] = useState('')
const [passwordConf, setPasswordConf] = useState('')//パスワード確認用のstate
const [error, setError] = useState('')
const navigate = useNavigate();
const Registerの型は、React.FC<Props>となります。Propsは先のtypeの箇所で設定した型定義です。
stateについては、パスワード用のpassword、確認用に再入力するパスワード用のpasswordConf、そしてerrorはApp.tsxから受け取るのではなく、(errorのステート管理が複雑になるので)ローカルのものとして定義してます。
そして、const navigate として、React RouterのuseNavigateを使用しています。useNavigateはページ遷移をする際に使用されます。navigate()の引数に遷移先のパスを渡すことでページを遷移する事ができます。
次に、Supabaseのサインアップ処理についてです。
// /src/pages/Register.tsx
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== passwordConf) {//2回入力されたパスワード一致確認
setError('パスワードが一致しません');
return;
} else if (password.length < 6) {//パスワード要件充足確認
setError('パスワードは6文字以上にしてください');
return;
}
else {
setLoading(true);//ローディング状態セット
try {//Supabaseサインアップ処理
const { error: signUpError } = await supabase.auth.signUp({
email: email,
password: password,
})
if (signUpError) {
throw signUpError;
}
alert('登録完了メールを確認してください');
navigate('/'); // signUpが成功した場合にのみページ遷移
}
catch (error) {
setError('サインアップに失敗しました');
}
finally {
setLoading(false);
}
}
};
const onSubmit として定義しています。formイベントをサブミットした際に実行されます。
なお、タイムラグが発生しますので、非同期通信のasync/awaitを用いています。
処理としては、まず、入力したパスワードと確認用に入力したパスワードの一致チェックをしています。次にパスワード要件(任意の6文字以上)を満たしているかのチェックです。この6文字以上は、Supabaseのデフォルト設定です。
パスワードに問題なければ、ローディング状態をセットし、Supabase処理機能として作成したsupabaseClient.tsにより、supabase.auth.signUpを実行しています(コード冒頭のインポート箇所、import { supabase } from “../supabaseClient” でsupabaseをインポートしています)。
処理に成功すれば、’登録完了メールを確認してください’とアラートを表示、次に、navigate(‘/’);でルートURL(/)に飛ばしています。
が、現段階では、’/’のルートは設定していないので、「みつかりません」と表示される事になりますが、後ほど、設定しますので、今はこのままとしておいてください。
エラーの場合は、setErrorでエラー内容をセットし、表示。最後にローディング状態を解除(false)しています。
Supabaseのパスワード要件については、Supabaseサイトのプロジェクトの設定(左側メニューの歯車アイコン) > Authentication > Passwords の箇所で設定されています。カスタマイズする場合はそこを変更してください。
続いて、JSXの箇所です。
// /src/pages/Register.tsx
return (
<>
<Flex alignItems='center' justify='center' p={5}>
<Card px={5}>
<CardHeader>
<Heading size='md' textAlign='center'>新規登録</Heading>
</CardHeader>
<CardBody w={{ base: 'xs', md: 'lg' }}>
<form onSubmit={onSubmit}>//submitでonSubmitを実行
<InputGroup>
<InputLeftElement pointerEvents='none'>
<FaUserCheck color='gray' />
</InputLeftElement>
<Input
autoFocus
type='email'
placeholder='メールアドレスを入力'
name='email'
value={email}
required
mb={2}
onChange={e => setEmail(e.target.value)}//入力値をmailにセット
onFocus={() => setError('')}//再フォーカスでエラー初期化
/>
</InputGroup>
<Text fontSize='12px' color='gray'>パスワードは6文字以上</Text>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<RiLockPasswordFill color='gray' />
</InputLeftElement>
<Input
type='password'
placeholder='パスワードを入力'
name='password'
value={password}
required
mb={2}
onChange={e => setPassword(e.target.value)}//入力値をpasswordにセット
onFocus={() => setError('')}//再フォーカスでエラー初期化
/>
</InputGroup>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<RiLockPasswordFill color='gray' />
</InputLeftElement>
<Input
type='password'
placeholder='パスワードを入力(確認)'
name='password'
value={passwordConf}
required
mb={2}
onChange={e => setPasswordConf(e.target.value)}//入力値をpasswordConfにセット
onFocus={() => setError('')}//再フォーカスでエラー初期化
/>
</InputGroup>
{error && <Box color='red'>{error}</Box>}//エラーがあれば、エラー表示
<Box mt={4} mb={2} textAlign='center'>
<Button
type="submit"
isLoading={loading}
loadingText='Loading'
spinnerPlacement='start'
colorScheme='cyan'
variant='outline'
mx={2}>登録する</Button>//ボタンクリックでsubmit
<Button
colorScheme='gray'
onClick={() => window.history.back()}
mx={2}
>戻る</Button>
</Box>
</form>
</CardBody>
</Card>
</Flex>
</>
)
}
新規登録用のformを設置しています。その中にメールアドレス、パスワード、パスワード(確認用)のinputフィールドを設けています。それぞれのinputフィールドは、onChangeイベントで、mail、password、passwordConfをset関数でstate変更を実施しています。
エラーが発生した場合は、{error && <Box color=’red’>{error}</Box>}でエラー表示します。なお、エラー発生した場合、各inputフィールドに再フォーカス(カーソルを移動)することでエラー表示を消す処理をしています。
「登録する」ボタンクリックで、formがサブミットされ、先のSupabaseサインアップ処理のonSubmitが実行されます。
なお、formによるサブミットで処理を実行は、ボタンなどのonClickイベント等でも実現可能ですが、inputフィールドのrequiredオプションを機能させる(requiredが入力されていない場合、ブラウザの機能で入力エラーを表示する)ためには、formサブミットが必要となりますので、この実装としました。
3.2 サインアップ機能の確認
では、実際にRegister.tsxを利用してSupabaseのサインアップ機能の実行を確認してきます。
まず、Register.tsxを表示出来るようにして上げる必要がありますので、App.tsxのルート設定を追加します。App.tsxを以下のように変更します。
// /src/App.tsx
import { useState } from "react";
import { Link, Route, Routes } from "react-router-dom";
import { Box, Card, Flex } from "@chakra-ui/react"
import { StudyData } from "./studyData";
import { supabase } from "./supabaseClient";
import Home from "./pages/Home";
import Register from "./pages/Register";//Register.tsxのインポート追加
まずインポートの箇所に、Register.tsxのインポートを追加します。
// /src/App.tsx
<Routes>
<Route path='/home' element={
<Home
learnings={learnings} loading={loading} setLoading={setLoading} error={error}
setError={setError} deleteDb={deleteDb}
insertDb={insertDb} updateDb={updateDb}
calculateTotalTime={calculateTotalTime}
fetchLearnings={fetchLearnings} email={email} />
}/>
<Route path='/register' element={//Registerの追加
<Register
email={email} setEmail={setEmail} loading={loading} setLoading={setLoading} />
}/>
<Route
path="*"
element={
<Flex justifyContent='center' alignItems='center' p='5'>
<Card p='10'>
みつかりません<br />
<Box
as='span'
textDecoration='none'
_hover={{ textDecoration: 'underline' }}
>
<Link to='/'>トップページへ</Link>
</Box>
</Card>
</Flex>
}
/>
</Routes>
続いて、JSX内に、Routeとして、パス’/register’を追加し、パス先として、Registerをセットする記述をします。以下内容です。
<Route path=’/register’ element={
<Register
email={email} setEmail={setEmail} loading={loading} setLoading={setLoading} />
} />
URLパスは/register、Registerに渡すPropsは、email, setEmail, loading, setLoading となります。
この時点でのApp.tsxコード全体は以下となります。
// /src/App.tsx
import { useState } from "react";
import { Link, Route, Routes } from "react-router-dom";
import { Box, Card, Flex } from "@chakra-ui/react"
import { StudyData } from "./studyData";
import { supabase } from "./supabaseClient";
import Home from "./pages/Home";
import Register from "./pages/Register";//Register.tsxのインポート追加
function App() {
const [learnings, setLearnings] = useState<StudyData[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [email, setEmail] = useState('test@test.com');
// Supabaseからデータを取得する関数
const fetchLearnings = async () => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.select('*')
.eq('email', [email])//email追加、配列として渡す
if (error) {
console.error('Error fetching data:', error);
console.log('データの読込に失敗しました', error);
setError(`データの読込に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('データ読み込み完了', data);
setLearnings(data);
setLoading(false);
}
};
//DB新規登録
const insertDb = async (learning: StudyData) => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.insert([
{ title: learning.title, time: learning.time, email: email },//email追加
])
.select();
if (error) {
setError(`データの更新に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('insert', data);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
//DB更新
const updateDb = async (learning: StudyData) => {
setLoading(true);
const { data, error } = await supabase
.from('learning_record_auth')
.update({ title: learning.title, time: learning.time })
.eq('id', learning.id)
.select()
if (error) {
console.error('Error insert data:', error);
setError(`データの更新に失敗しました、${error.message}`);
setLoading(false);
} else {
console.log('update', data);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
//DBデータ削除
const deleteDb = async (learning: StudyData) => {
setLoading(true);
const { error } = await supabase
.from('learning_record_auth')
.delete()
.eq('id', learning.id);
if (error) {
console.error('Error delete data:', error);
setLoading(false);
} else {
console.log('delete', learning.id);
fetchLearnings();//登録後、データ全体を再表示、ローディング解除
}
}
const calculateTotalTime = () => {
return learnings.reduce((total, learning) => total + learning.time, 0);
};
return (
<Routes>
<Route path='/home' element={
<Home
learnings={learnings} loading={loading} setLoading={setLoading} error={error}
setError={setError} deleteDb={deleteDb}
insertDb={insertDb} updateDb={updateDb}
calculateTotalTime={calculateTotalTime}
fetchLearnings={fetchLearnings} email={email} />
} />
<Route path='/register' element={
<Register
email={email} setEmail={setEmail} loading={loading} setLoading={setLoading} />
} />
<Route
path="*"
element={
<Flex justifyContent='center' alignItems='center' p='5'>
<Card p='10'>
みつかりません<br />
<Box
as='span'
textDecoration='none'
_hover={{ textDecoration: 'underline' }}
>
<Link to='/'>トップページへ</Link>
</Box>
</Card>
</Flex>
} />
</Routes>
)
}
export default App
では、実際に、Register.tsxの動きを確認しましょう。
http://localhost:5173/register にアクセスします(開発サーバを停止している場合は、ターミナルでnpm run devを実行し、起動してください)。
以下の画面が表示されますので、メールアドレスは、先の1.4 テーブルの準備で設定したメールアドレスを入力します。パスワードは適切なものを入力してください。
入力後、登録するをクリックします。Supabaseでの処理が正常終了すれば、「登録完了メールを確認してください」とアラートが表示されます。
この時点で、Supabaseのサイトを確認してみましょう。1.2 新規登録時のメールテンプレートの箇所で記載した通り、Supabase上のAuthentication > Usersを選択すると、登録されたユーザ一覧が表示されますが、まだリンクでの認証が済んでいない場合は、Last Sign Inの列でWaiting gor verificationと黄色で表示されてると思います。
続いて、入力したメールアドレスにてSupabaseから送信されたメールを確認します。下記のような登録確認メールが届きますので、登録完了をクリックしましょう。クリック後は、http://localhost:5173/にリダイレクトされます。この時点では、「/」のルート設定はされていない為、「みつかりません」と表示されますが、問題ありません。
登録完了クリック後、Supabase上のAuthentication > Usersを確認すると、Last Sign Inの列でWaiting gor verificationの表示が変わり、登録完了した時間が、サインイン時間として表示されていると思います。これで登録認証完了です。
3.3 Supabaseポリシーの変更
ユーザー登録が完了しましたので、1.5 ポリシーの作成の箇所で、動作確認の為に暫定設定としていたポリシー設定を変更します。
Target Roles:anonと、authenticated
としてましたが、
Target Roles:authenticated
のみとします。
以上でサインアップ機能の開発・実装は完了です。前編はここまでとします。
次回、後編では、ログイン・ログアウト、パスワードリセット機能について解説します。
後編公開しました!こちらです。
No responses yet