✍️

React NativeとSupabaseで自動ログイン付きTodoアプリを作ろう

に公開

はじめに

このハンズオンでは、React NativeとSupabaseを使って、ユーザーのログイン情報を安全に保持し、アプリ起動時に自動でログインできるTodoアプリの作り方を学びます。

完成形

ユーザーはメールアドレスとパスワードでログインでき、一度ログインすれば、アプリを閉じても再度ログインする必要はありません。

開発環境

このハンズオンは、以下の環境で開発を進めていきます。

  • Node.js v18.17.0
  • npm 9.6.7
  • Expo CLI 6.3.10
  • React Native
  • Supabase

Supabaseのセットアップ

まずは、Supabaseのプロジェクトを作成し、データベースの設定を行います。

1. Supabaseプロジェクトの作成

Supabase公式サイトにアクセスし、新しいプロジェクトを作成します。

2. todosテーブルの作成

プロジェクトが作成できたら、SQL Editorに移動し、以下のクエリを実行してtodosテーブルを作成します。

create table todos (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users not null,
  task text not null,
  is_completed boolean default false,
  inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);

alter table todos enable row level security;

create policy "Users can insert their own todos"
on todos for insert to authenticated with check (auth.uid() = user_id);

create policy "Users can view their own todos"
on todos for select to authenticated using (auth.uid() = user_id);

create policy "Users can update their own todos"
on todos for update to authenticated using (auth.uid() = user_id);

create policy "Users can delete their own todos"
on todos for delete to authenticated using (auth.uid() = user_id);

React Nativeのセットアップ

次に、React Nativeのプロジェクトをセットアップします。

npx create-expo-app todo-app
cd todo-app

Supabaseへの接続設定

React NativeアプリからSupabaseに接続するための設定を行います。

1. 必要なライブラリのインストール

npm install @supabase/supabase-js @react-native-async-storage/async-storage

2. Supabaseクライアントの作成

lib/supabase.tsというファイルを作成し、Supabaseクライアントを初期化します。

lib/supabase.ts
import 'react-native-url-polyfill/auto'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'YOUR_SUPABASE_URL'
const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY'

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
})

YOUR_SUPABASE_URLYOUR_SUPABASE_ANON_KEYは、Supabaseのプロジェクト設定ページから取得してください。

ログイン機能の実装

components/Auth.tsxを作成し、ログインフォームを実装します。

components/Auth.tsx
import React, { useState } from 'react'
import { Alert, StyleSheet, View, AppState, Button, TextInput } from 'react-native'
import { supabase } from '../lib/supabase'

// Tells Supabase Auth to continuously refresh the session automatically if
// the app is in the foreground. When this is added, you will continue to receive
// `onAuthStateChange` events with the `TOKEN_REFRESHED` or `SIGNED_OUT` event
// if the user's session is terminated. This should only be registered once.
AppState.addEventListener('change', (nextAppState) => {
  if (nextAppState === 'active') {
    supabase.auth.startAutoRefresh()
  } else {
    supabase.auth.stopAutoRefresh()
  }
})

export default function Auth() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)

  async function signInWithEmail() {
    setLoading(true)
    const { error } = await supabase.auth.signInWithPassword({
      email: email,
      password: password,
    })

    if (error) Alert.alert(error.message)
    setLoading(false)
  }

  async function signUpWithEmail() {
    setLoading(true)
    const {
      data: { session },
      error,
    } = await supabase.auth.signUp({
      email: email,
      password: password,
    })

    if (error) Alert.alert(error.message)
    if (!session) Alert.alert('Please check your inbox for email verification!')
    setLoading(false)
  }

  return (
    <View style={styles.container}>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <TextInput
          onChangeText={(text) => setEmail(text)}
          value={email}
          placeholder="email@address.com"
          autoCapitalize={'none'}
        />
      </View>
      <View style={styles.verticallySpaced}>
        <TextInput
          onChangeText={(text) => setPassword(text)}
          value={password}
          secureTextEntry={true}
          placeholder="Password"
          autoCapitalize={'none'}
        />
      </View>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
      </View>
      <View style={styles.verticallySpaced}>
        <Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
      </View>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    marginTop: 40,
    padding: 12,
  },
  verticallySpaced: {
    paddingTop: 4,
    paddingBottom: 4,
    alignSelf: 'stretch',
  },
  mt20: {
    marginTop: 20,
  },
})

Todoリスト機能の実装

ログイン後に表示されるTodoリストの画面をcomponents/TodoList.tsxに実装します。

components/TodoList.tsx
import React, { useState, useEffect } from 'react'
import { View, Text, TextInput, Button, FlatList, StyleSheet } from 'react-native'
import { supabase } from '../lib/supabase'
import { Session } from '@supabase/supabase-js'

const TodoList = ({ session }: { session: Session }) => {
  const [todos, setTodos] = useState<{ id: number; task: string; is_completed: boolean }[]>([])
  const [task, setTask] = useState('')

  useEffect(() => {
    const fetchTodos = async () => {
      const { data, error } = await supabase.from('todos').select('*')
      if (error) console.log('error', error)
      else setTodos(data)
    }
    fetchTodos()
  }, [])

  const addTodo = async () => {
    const { data, error } = await supabase.from('todos').insert([{ task, user_id: session.user.id }]).select()
    if (error) console.log('error', error)
    else if (data) setTodos([...todos, ...data])
    setTask('')
  }

  const toggleTodo = async (id: number, is_completed: boolean) => {
    const { data, error } = await supabase.from('todos').update({ is_completed: !is_completed }).match({ id }).select()
    if (error) console.log('error', error)
    else if (data) {
      setTodos(todos.map((todo) => (todo.id === id ? data[0] : todo)))
    }
  }

  const deleteTodo = async (id: number) => {
    const { error } = await supabase.from('todos').delete().match({ id })
    if (error) console.log('error', error)
    else setTodos(todos.filter((todo) => todo.id !== id))
  }

  return (
    <View style={styles.container}>
      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          value={task}
          onChangeText={setTask}
          placeholder="Add a new task"
        />
        <Button title="Add" onPress={addTodo} />
      </View>
      <FlatList
        data={todos}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <View style={styles.todoContainer}>
            <Text style={[styles.task, item.is_completed && styles.completed]}>{item.task}</Text>
            <Button title={item.is_completed ? 'Undo' : 'Complete'} onPress={() => toggleTodo(item.id, item.is_completed)} />
            <Button title="Delete" onPress={() => deleteTodo(item.id)} />
          </View>
        )}
      />
      <Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  inputContainer: {
    flexDirection: 'row',
    marginBottom: 20,
  },
  input: {
    flex: 1,
    borderColor: 'gray',
    borderWidth: 1,
    padding: 10,
  },
  todoContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 10,
  },
  task: {
    flex: 1,
  },
  completed: {
    textDecorationLine: 'line-through',
    color: 'gray',
  },
})

export default TodoList

自動ログインの実装

App.tsxを編集し、アプリ起動時にログイン状態をチェックし、自動ログインを実現します。

App.tsx
import 'react-native-url-polyfill/auto'
import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'
import Auth from './components/Auth'
import TodoList from './components/TodoList'
import { View } from 'react-native'
import { Session } from '@supabase/supabase-js'

export default function App() {
  const [session, setSession] = useState<Session | null>(null)

  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
    })

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
  }, [])

  return (
    <View>
      {session && session.user ? <TodoList key={session.user.id} session={session} /> : <Auth />}
    </View>
  )
}

おわりに

このハンズオンでは、React NativeとSupabaseを使って、自動ログイン機能付きのTodoアプリを作成しました。Supabaseの認証機能とAsyncStorageを組み合わせることで、簡単に安全な自動ログインを実装できます。

ぜひこのハンズオンを参考に、自分だけのアプリ開発に挑戦してみてください。

Discussion