Supabase勉強のためにReact Nativeを使って遊んでみた

2022.08.08

こんにちは、こんばんわ。 「コロナワクチン3回目接種後、左大胸筋が痛いです。」 CX 事業本部 Delivery 部 MAD グループ@札幌の hiro です。

今回は、弊社ブログでもいくつか既に紹介されているSupabaseを勉強がてら触ってみたということで、執筆させていただきます。

はじめに

既に、いくつか弊社でもSupabaseに関する内容は執筆されており、大変わかりやすい内容となっています。 これらの情報を理解するためには、読むだけではなく、手を動かしてみようということで、 「Supabase」を触りながら、勉強したときの内容を執筆しています。

実装内容はシンプルに、「ログイン/ログアウト/サインアップ/サインアウトして、Supabaseとやりとりできているかなー?」を確認しています。

React Native for Webを利用して、React Native環境を整えつつ、 react-query/dotenv-cli/react-router-domを利用して構築しています。

□ 注意

いくつか注意点があります。

- Supabaseと、「ログイン/ログアウト/サインアップ/サインアウト」を通してやりとりしている内容になるので、細かい部分の制御は行なっていません
    例)ログインの時にバリデーションチェック文言いらない、直リンクした時の挙動など
- Next.jsを利用している記事が多いですが、今回は利用していません
- 前回のバリデーション記事を土台に作成しているため、バリデーション部分の記述は省略させていただいております。
- style適用部分はソースが長くなり、見づらくなるのでここでは割愛しています。

前回のバリデーション記事がこちらになります。

ご了承ください。

事前準備と動作確認

□ 環境構築とライブラリのインストール

  • React Native for Web

以下を参考に、環境構築するとGOOD

Reactが分かれば難しくない! React Native for Web 入門

  • バリデーションに関する部分

以下を土台にしているので、参考にしていただけると助かります! react-hook-form と yupを利用しています。

【React Native】 react-hook-form と yupを使ってフォームのバリデーションチェック

$ yarn add react-hook-form yup
  • Supabase
$ yarn add @supabase/supabase-js
  • react-router-dom

v6を導入しています。

$ yarn add react-router-dom
  • React Query

npmで導入すると、エラー吐くので注意。 npmでreact-query導入

$ yarn add react-query
  • dotenv-cli

.env情報を定義、取得するためのもの。 以下の記事を参考するとGOOD

Reactの環境変数をdotenv-cliで切り替えてみた

$ yarn add dotenv-cli

□ 動作確認

まずは、動作しているところから見ていただければと思います。 以下のように動作していることが確認できます。

◇ サインアップ

成功すると、下記図のようにsupabaseのAuthentication/Usersに登録されていることがわかります。

◇ ログインとログアウト

上記のように、supabaseのAuthentication/Usersに登録されていると、以下のようにログインできることがわかります。 今回はメール認証をおこなっているので、サインアップで登録したメールアドレスにメールが届いていることが確認できるはずです。

やってみよう

やってみようということで、ファイル構成やファイル内容を記述していきたいと思います。

□ ファイル構成

以下のファイル構成で、記述します。

.
├─── src
│   ├── component
│   │   └── TestInputComponent.tsx・・・・・・・・フォーム入力部分の表示
│   ├── hooks
│   │   └── useMutationAuth.ts・・・・・・・・Supabase auth signin/up
│   ├── pages
│   │   └── Home.tsx・・・・・・・・ログインやサインアップ後の画面の表示
│   ├── schema
│   │   └── testInputSchema.ts・・・・・・・・バリデーション処理管理部分
│   ├── utils
│   │   └── supabase.ts・・・・・・・・Supabaseクライアント作成
│   ├── App.tsx・・・・・・・・ログインやサインアップ画面の表示/認証イベント検知/ログインユーザ検知
│   └── index.tsx
└── .env.local

□ Routing設定

Queryクライアントは、以下オプションをfalse状態としています。

- データが取得できない場合に自動でリトライを行うオプション
- 一度そのウィンドウから外れ外側をクリックして再度ウィンドウに戻りクリックを行うとリフェッチが行われるオプション

index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import { QueryClient, QueryClientProvider } from "react-query";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnWindowFocus: false,
    },
  },
});

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<App />} />
          <Route path="/home" element={<Home />} />
        </Routes>
      </BrowserRouter>
    </QueryClientProvider>
  </React.StrictMode>
);

reportWebVitals();

□ SupabaseのAPI情報管理

.env.localへ、Supabaseで取得したAPI情報などを記述していきます。 Supabaseの連携については、以下の記事を参考に。

オープンソースで話題のBaaS「Supabase」を使ってみた

.env.local

REACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL
REACT_APP_SUPABASE_API_KEY=YOUR_SUPABASE_ANON_KEY

また、環境ごとにビルドする内容を変更するために、package.jsonの内容を以下のように変更します。

package.json

{
  "scripts": {
    "start": "react-scripts start",
    "build-local": "dotenv -e .env.local react-scripts build",
    "build-development": "dotenv -e .env.development react-scripts build",
    "build-staging": "dotenv -e .env.staging react-scripts build",
    "build-production": "dotenv -e .env.production react-scripts build",
  },

□ Supabaseクライアントの作成

.env.localに保存したSupabase情報を取得し、Supabaseクライアントを作成します。

supabase.ts

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

export const supabase = createClient(
    process.env.REACT_APP_SUPABASE_URL as string,
    process.env.REACT_APP_SUPABASE_API_KEY as string
)

□ ユーザ作成やログイン処理

Supabaseへのユーザ作成やログイン処理を実行できるhooksを作成していきます。

useMutationAuth.ts

import { useState } from 'react'
import { useMutation } from 'react-query';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../utils/supabase'

export const useMutationAuth = () => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const navigate = useNavigate();

    // ログイン失敗時のログイン情報初期化
    const resetLoginInfo = () => {
      setEmail('');
      setPassword('');
    }

    const login = useMutation(
      async () => {
        const {error} = await supabase.auth.signIn({email, password});
        if(error){
            throw new Error(error?.message);
        }
       },{
        onError: (error: any) => {
          alert(error);
          resetLoginInfo();
        }
       }
    );
    const register = useMutation(
      async () => {
        const { error } = await supabase.auth.signUp({email, password})
        if (error){
          throw new Error(error?.message)
        }
        navigate("/home")
      },
      {
        onError: (error: any) => {
          alert(error);
          resetLoginInfo();
        },
      }
    );
    return {
        email,
        setEmail,
        password,
        setPassword,
        login,
        register
    }
}

□ ログインやサインアップ画面の表示/認証イベント検知/ログインユーザ検知

ログインやサインアップするための画面表示をしています。 また、Supabaseによる、認証イベント(サインインやサインアウト)を検知やログインしているユーザを検知し、特定ページへリダイレクトさせます。

App.tsx

import React, { useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useForm, Control, FieldValues, SubmitHandler } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { TestInputComponent } from "./component/TestInputComponent";
import { testInputSchema } from "./schema/testInputSchema";
import { useMutationAuth } from "./hooks/useMutationAuth";
import { useLocation, useNavigate } from "react-router-dom";
import { supabase } from "./utils/supabase";

type FormDataInfo = {
  email: string;
  password: string;
};

export const App = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const [isLogin, setIsLogin] = useState<boolean>(true);
  const { email, setEmail, password, setPassword, login, register } =
    useMutationAuth();
  const { control, handleSubmit } = useForm<FormDataInfo>({
    resolver: yupResolver(testInputSchema),
  });

  const handleLogin = () => {
    // ログイン処理とユーザ登録処理条件分岐
    if (isLogin) {
      login.mutate();
    } else {
      register.mutate();
    }
  };
  const onSubmit: SubmitHandler<FormDataInfo> = async (data: any, e: any) => {
    await setEmail(data.email);
    await setPassword(data.password);
    handleLogin();
  };

  const validateSession = async () => {
    const user = supabase.auth.user();
    if (user && location.pathname === "/") {
      navigate("/home");
    } else if (!user && location.pathname !== "/") {
      await navigate("/");
    }
  };

  supabase.auth.onAuthStateChange((event, _) => {
    if (event === "SIGNED_IN" && location.pathname === "/") {
      navigate("/home");
    }
    if (event === "SIGNED_OUT") {
      navigate("/");
    }
  });

  useEffect(() => {
    validateSession();
  }, []);

  return (
    <View style={styles.container}>
      <TestInputComponent
        control={control as unknown as Control<FieldValues>}
        areaName="email"
        label="メールアドレス"
        placeholder="メールアドレス"
        autoCompleteType="email"
        autoCapitalize="none"
        style={styles.input}
        value={email}
      />
      <TestInputComponent
        control={control as unknown as Control<FieldValues>}
        areaName="password"
        label="パスワード"
        placeholder="パスワード"
        autoCompleteType="password"
        secureTextEntry
        style={styles.input}
        value={password}
      />
      <Pressable style={styles.button} onPress={() => handleSubmit(onSubmit)()}>
        <Text style={styles.text}>{isLogin ? "LOGIN" : "REGISTER"}</Text>
      </Pressable>
      <Pressable onPress={() => setIsLogin(!isLogin)}>
        <Text>{!isLogin ? "LOGIN" : "REGISTER"}</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  // 長くなるので省略しています。すみません。
});

export default App;

□ ログイン後遷移画面

サインアウトボタンを押下すると、サインアウトを実施します。

Home.tsx

import React from "react";
import { View, StyleSheet, Pressable, Text } from "react-native";
import { supabase } from "../utils/supabase";

const Home = () => {
  const signOut = () => {
    supabase.auth.signOut();
  };
  return (
    <View style={styles.container}>
      <Text style={styles.title}>HELLO</Text>
      <Pressable style={styles.button} onPress={() => signOut()}>
        <Text style={styles.text}>SIGN OUT</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  // 長くなるので省略しています。すみません。
});

export default Home;

□ ビルドしてドン

記述した内容で、ビルドとサーバーを動かします。

$ npm run build-local
$ npm start

おわりに

Supabaseを最近はよく見るようになって、一度は触れてみたいと思っていたので、今回触ることができてよかったです。

所感としてはかなり使いやすいです。 Supabaseで認証周りを楽にするのであれば、以下の記事を参考にするのが良いと思います。

Supabaseのauth helperによってNext.jsプロジェクトの認証周りを楽にする

少々長くなってしまいましたが、ありがとうございました。