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公式サイトにアクセスし、新しいプロジェクトを作成します。
todos
テーブルの作成
2. プロジェクトが作成できたら、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クライアントを初期化します。
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_URL
とYOUR_SUPABASE_ANON_KEY
は、Supabaseのプロジェクト設定ページから取得してください。
ログイン機能の実装
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
に実装します。
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
を編集し、アプリ起動時にログイン状態をチェックし、自動ログインを実現します。
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